LVGL high CPU usage with NRF24 transciever

Hello, I have a project where I use LVGL to display fancy graphics, I’ve been working with LVGL for quite a while and have successful projects with it.

This specific project uses an NRF24 transciever to receive data and display the numbers on the screen. Receive happens with an interrupt pin, when the pin is in FALLING state the function to print the receive is called. The MCU spends only 92 microseconds in the function so it’s almost no time at all. Right now I send from the transmitter every 250ms. So with that said, LVGL has all the time in the world to do it’s thing. I’m using a Teensy running 396mhz.

So I boot the device up but have the transmitter off, everything works fine, CPU usage at 3%. It can stay this way forever. I turn on the transmitter and data starts flowing in, after about 10 seconds CPU rises to 97% and the device is basically frozen. I turn of the transmitter but this high CPU usage stays the same until I reboot the device.

I am only displaying 2 numbers on the screen, and if I comment those out and only display the numbers in Serial.print, CPU usage still spikes.

I have no idea why this occurs. If I disable LVGL alltogether, this doesn’t happen at all. So I narrowed it down a bit more. If I comment out “lv_disp_drv_register(&disp_drv);” the CPU usage is nice and low.

I understand that this isn’t an LVGL fault or anything like that. But LVGL and the NRF24 doesn’t seem to like eachother.

Any ideas are appreciated as I don’t know what to do at this point.

EDIT: Further testing (this will blow your mind).
Crash occurs after about 10 seconds, so I activated the logging of LVGL and used TRACE to see everything. Now it can run for about 90 seconds until freeze. I thought it was just an odd coincidence so I reverted back to nog logging and once again freeze after 10 seconds. And so I activated the logging once again and saved the last part of it when it froze.

The NRF24 library and the Teensy core SPI library both have while loops in them. I think that LVGL is sensitive to these loops and if everything is not 100% right with timing, LVGL will freeze. I hope that someone knows a nice workaround for this issue.

Here is the log:

[Trace] (97.337, +0)     lv_mem_buf_release: begin (address: 0x2000f648)        (in lv_mem.c line #320)

[Trace] (97.337, +0)     lv_mem_buf_get: begin, getting 40 bytes        (in lv_mem.c line #266)

[Trace] (97.337, +0)     lv_mem_buf_get: returning already allocated buffer (buffer id: 0, address: 0x2000f648)         (in lv_mem.c line #289)

[Trace] (97.337, +0)     lv_mem_buf_release: begin (address: 0x2000f648)        (in lv_mem.c line #320)

[Trace] (97.337, +0)     lv_mem_buf_get: begin, getting 64 bytes        (in lv_mem.c line #266)

[Trace] (97.337, +0)     lv_mem_buf_get: returning already allocated buffer (buffer id: 0, address: 0x2000f648)         (in lv_mem.c line #289)

[Trace] (97.337, +0)     lv_mem_buf_release: begin (address: 0x2000f648)        (in lv_mem.c line #320)

[Trace] (97.337, +0)     lv_mem_buf_get: begin, getting 99 bytes        (in lv_mem.c line #266)

[Trace] (97.337, +0)     lv_mem_buf_get: returning already allocated buffer (buffer id: 0, address: 0x2000f648)         (in lv_mem.c line #289)

[Trace] (97.337, +0)     lv_mem_buf_release: begin (address: 0x2000f648)        (in lv_mem.c line #320)

[Trace] (97.337, +0)     lv_mem_buf_get: begin, getting 9 bytes         (in lv_mem.c line #266)

[Trace] (97.337, +0)     lv_mem_buf_get: returning already allocated buffer (buffer id: 0, address: 0x2000f648)         (in lv_mem.c line #289)

[Trace] (97.337, +0)     lv_mem_buf_release: begin (address: 0x2000f648)        (in lv_mem.c line #320)

[Trace] (97.337, +0)     event_send_core: Sending event 22 to 0x2000f578 with 0x2004fe40 param  (in lv_event.c line #405)

[Trace] (97.337, +0)     event_send_core: Sending event 23 to 0x2000f578 with 0x2004fe40 param  (in lv_event.c line #405)

[Trace] (97.337, +0)     event_send_core: Sending event 24 to 0x2000f578 with 0x2004fe40 param  (in lv_event.c line #405)

[Trace] (97.337, +0)     event_send_core: Sending event 44 to 0x2000f578 with 0x2004fbfc param  (in lv_event.c line #405)

[Trace] (97.337, +0)     event_send_core: Sending event 44 to 0x2000f578 with 0x2004fbfc param  (in lv_event.c line #405)

[Trace] (97.337, +0)     event_send_core: Sending event 25 to 0x2000f578 with 0x2004fe40 param  (in lv_event.c line #405)

[Trace] (97.337, +0)     event_send_core: Sending event 23 to 0x2000e2a0 with 0x2004fe80 param  (in lv_event.c line #405)

[Trace] (97.337, +0)     event_send_core: Sending event 24 to 0x2000e2a0 with 0x2004fe80 param  (in lv_event.c line #405)

[Trace] (97.337, +0)     event_send_core: Sending event 25 to 0x2000e2a0 with 0x2004fe80 param  (in lv_event.c line #405)

[Trace] (97.337, +0)     call_flush_cb: Calling flush_cb on (0;438)(138;479) area with 0x20225800 image pointer         (in lv_refr.c line #979)

[Trace] (97.337, +0)     lv_mem_free: freeing 0x2000f648        (in lv_mem.c line #161)

[Trace] (97.337, +0)     _lv_disp_refr_timer: finished  (in lv_refr.c line #316)

[Trace] (97.337, +0)     lv_timer_exec: timer callback 0x7a21 finished  (in lv_timer.c line #314)

[Trace] (97.337, +0)     lv_timer_handler: finished (1 ms until the next timer call)    (in lv_timer.c line #144)

Code to reproduce:

#include "Arduino.h"
#include <SPI.h>
#include <RF24.h>
#include "../lib/ILI948x_t4_mm/ILI948x_t4_mm.h" 
#include "../lib/lvgl/lvgl.h"

int NRF_IRQ = 1;

long powerPressedStartTime = 0;

RF24 NRF24(0, 10); // Declare object from nRF24 library (Create your wireless SPI)

bool readyToRock = false;
void NRF24_InterruptReceive();

#define LVGL_TICK_PERIOD 1
IntervalTimer tick;

const uint16_t screenWidth = 320;
const uint16_t screenHeight = 480;

ILI948x_t4_mm lcd = ILI948x_t4_mm(17, 16, 5);

/*A static or global variable to store the buffers*/
static lv_disp_draw_buf_t disp_buf;

//LVGL two half screen sized buffers in DMA memory
DMAMEM static lv_color_t buf_1[(screenWidth * screenHeight)/2];
DMAMEM static lv_color_t buf_2[(screenWidth * screenHeight)/2];

static lv_disp_drv_t disp_drv;

const uint64_t pAddress = 0xB00B1E5000LL;  //Create a pipe addresses for the 2 nodes to communicate over, the "LL" is for LongLong type

void my_log_cb(const char * buf) {
  Serial.println(buf);
}

static void lv_tick_handler(void) {
  lv_tick_inc(LVGL_TICK_PERIOD);
}

FASTRUN void my_flush_cb(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p) {
  arm_dcache_flush((void*)buf_1, sizeof(buf_1)); // always flush cache after writing to DMAMEM variable that will be accessed by DMA
  arm_dcache_flush((void*)buf_2, sizeof(buf_2)); // always flush cache after writing to DMAMEM variable that will be accessed by DMA
  lcd.pushPixels16bitDMA((uint16_t*)(color_p),area->x1, area->y1, area->x2, area->y2);
}

FASTRUN void flushCB() {
  lv_disp_flush_ready(&disp_drv);
}

void NRF24_Initiate() {
  NRF24.begin();                                    // Start the nRF24 module
  NRF24.maskIRQ(1,1,0);                             // mask all IRQ triggers except for receive (1 is mask, 0 is no mask)
  NRF24.setPALevel(RF24_PA_LOW);                    // Set power level to low, won't work well at higher levels (interfer with receiver)
  NRF24.openReadingPipe(1, pAddress);               // open pipe o for recieving meassages with pipe address
  NRF24.startListening();                           // Start listening for messages
  pinMode(NRF_IRQ, INPUT);
  attachInterrupt(NRF_IRQ, NRF24_InterruptReceive, FALLING);  //Create interrupt: 0 for pin 2 or 1 for pin 3, the name of the interrupt function or ISR, and condition to trigger interrupt
}

void NRF24_InterruptReceive() {
  long int t1 = micros();


  uint8_t payloadArray[12];
  NRF24.read(&payloadArray, sizeof(payloadArray)); // RECEIVE DATA INTO BYTE ARRAY, THIS IS CAUSING FREEZE.

  long int t2 = micros();
  Serial.print("Time taken by the task: "); Serial.print(t2-t1); Serial.println(" microseconds");
}

void setup(){
  Serial.begin(57600);  //start serial to communicate process
  Serial.print(CrashReport);

  pinMode(7, OUTPUT); // RDX
  digitalWrite(7, HIGH); // Pull RDX high
    
  lcd.begin(24);
  lcd.setFrameRate(68);
  lcd.setRotation(2);
  lcd.invertDisplay(true);
  lcd.onCompleteCB(&flushCB);

  pinMode(34, OUTPUT); // LCD BACKLIGHT
  digitalWrite(34, HIGH); // LCD BACKLIGHT

  lv_log_register_print_cb(my_log_cb); // LOGGING OF LVGL
 
  lv_init();
  
  lv_disp_draw_buf_init(&disp_buf, buf_1, buf_2, (screenWidth * screenHeight)/2);
  lv_disp_drv_init(&disp_drv);
  disp_drv.flush_cb = my_flush_cb;
  disp_drv.draw_buf = &disp_buf;
  disp_drv.hor_res = screenWidth;
  disp_drv.ver_res = screenHeight;
  lv_disp_drv_register(&disp_drv);

  tick.begin(lv_tick_handler, LVGL_TICK_PERIOD * 1000);  // Start ticker
}

const int loopDelay1 = 3;
unsigned long loopTimeNow1 = 0;

void loop() {
  if (millis() > loopTimeNow1 + loopDelay1) {
    lv_timer_handler();

    loopTimeNow1 = millis();
  }

  if (!readyToRock && millis() > 5000) { // INITIATE AFTER EVERYTHING HAS BOOTED.
    NRF24_Initiate();

    readyToRock = true;
  }
}

Could both loops/threads be trying to access the same variable?

Have you tried making the variables volatile?

1 Like

That could be the case. Alltho I don’t think so, but worth a test for sure! I am only printing out the time it takes to execute at the moment. So no variable “crossing” is being made.

I did a new write up in the PJRC forum
About this. Explaining it in a better way. Here is what I posted there:

I have this very strange issue. I’m running a Teensy Micromod with a display with Little VGL (LVGL) library for graphics. The display is 8 bit parallel interface, no SPI. And the driver uses DMA on the Teensy. Works very well and I’m using it successfully for other projects as well. But for this project I’m also using an NRF24 2.4ghz transceiver via SPI. This also works just fine.

Now here’s the issue. When both LVGL and the NRF24 is running, I get random freezes. LVGL actually can’t freeze as it has a built in safeguard that makes the MCU crash instead. Crashes are good since CrashReport can explain what happened. This tells me that the freeze happens outside LVGL as it would otherwise crash. The MCU gets stuck somewhere in an infinite void. Memory usage is at 14% and CPU usage is between 0 and 1% so no overload there. This is measured by LVGL.

The NRF24 uses an interrupt pin and that calls a function every time I receive. Receiving a full 32 byte message takes ~70 microseconds. So it’s all happening very quickly. The transmitter only sends out a message frame every 250ms just for testing.

LVGL has a logging feature where it can spit our errors, warnings and also everything that’s happening. The latter one is called TRACE and it basically just spams every action LVGL is taking. LVGL has nothing to do with the NRF24 but I still wanted to run the log to see what happened.

Here comes the really weird stuff.
When the logging feature is enabled I get a few minutes of runtime until it freezes. With it disabled or set to only show critical warnings, I get way less runtime before it freezes. Obviously this has nothing to do with the logging itself but rather timing. A freeze means that the MCU is completely unresponsive. It won’t even go into BOOT mode from the flash tool. It needs a hard reset.

The log I’m posting below froze just after a timing event. So the only logical explanation is that something strange with timings is going on which causes LVGL to freeze. LVGL uses hardware based timing but it can also use millis(), I’ve tested both and the results are the same.

In a nutshell, NRF24 and LVGL cannot co-exist for some reason. If I disable the LVGL tick function, which ensures that LVGL can run its tasks, the NRF24 works fine. If I disable the NRF24 receive function then LVGL works just fine.

I have updated the original post and added a code example to reproduce the issue.

Hi,

In log this the last line before the crash?

[Trace] (97.337, +0)     lv_timer_handler: finished (1 ms until the next timer call)    (in lv_timer.c line #144)

What do you mean by that? I don’t know about such a feature.

1 Like

I mean that LVGL crashes if something is wrong. It doesn’t freeze.

This issue was solved by defragster in the PJRC forum. It was super simple ironically.

Instead of having the code in the interrupt function. I now have a flag in the interrupt and then the actual code in the loop() with that flag to trigger. And it works perfectly now!

1 Like

Happy to hear it’s solved! :slight_smile:

1 Like