c++esp32freertosarduino-esp32

How to use render task(s) for LED matrix with ESP32 FreeRTOS


I have an ESP32/Arduino application that drives, amongst others, an LED matrix. It uses SimpleFSM to implement the state machine handling user interaction, sensor input, etc.. Sometimes the LED matrix displays static "icons", sometimes it displays animations. The two use cases differ at code level conceptually:

What is the FreeRTOS-idiomatic way to implement the rendering part?

Initially I wanted to start a new task whenever something new has to be rendered but that caused nothing but pain. The old/previous task had to be terminated first before the new one starts. Besides, tasks should be long-running, ideally infinite.

So, it appears the FreeRTOS-way would be to start a single infinitely-running render task. On state change the main task would then tell it to stop doing what it is currently doing and start something else instead. How do you swap the "unit of work" of a task? How do you prevent the task from completing when it renders a static icon - other than rendering it over and over again? Should the render task maybe use it's own little state machine for that?


Solution

  • Should the render task maybe use it's own little state machine for that?

    Yes, the [single] render task should keep its own state.

    The render task should have a request/work queue that it polls.

    The main task does the user interaction. If it decides to change what is displayed, it queues a request to the render task.

    The render task polls the request queue. If a new request has arrived it starts that action. Otherwise, it continues to process the current/existing work request.


    Here is some pseudo code. Replace the queue related functions with the FreeRTOS queue equivalent functions:

    typedef enum {
        ACTION_IDLE,
    
        ACTION_ZERO,
        ACTION_ONE,
    
        ACTION_ANIM_CIRCLE,
        ACTION_ANIM_SQUARE,
        ACTION_ANIM_TRIANGLE,
    } action_t;
    
    typedef struct {
        action_t req_action;
        int req_state;
        // ...
    } request_t;
    
    void
    render_task(void)
    {
        request_t *reqcur = NULL;
        request_t *reqnew = NULL;
    
        while (1) {
            // new work to do?
            reqnew = dequeue_request_nowait(&render_task_work);
    
            // yes, release old and setup new work
            if (reqnew != NULL) {
                // release old request block to free queue
                if (reqcur != NULL)
                    enqueue_request(&render_task_free,reqcur);
    
                // setup new request
                reqcur = reqnew;
                init = 1;
            }
            else
                init = 0;
    
            // handle current work
            if (reqcur != NULL) {
                switch (reqcur->req_action) {
                case ACTION_IDLE:
                    break;
    
                case ACTION_ZERO:
                    if (init)
                        display_zeros();
                    reqcur->req_action = ACTION_IDLE;
                    break;
    
                case ACTION_ONE:
                    if (init)
                        display_ones();
                    reqcur->req_action = ACTION_IDLE;
                    break;
    
                case ACTION_ANIM_CIRCLE:
                case ACTION_ANIM_SQUARE:
                case ACTION_ANIM_TRIANGLE:
                    if (init)
                        reset_animation(reqcur);
                    display_animation_frame(reqcur);
                    advance_animation_state(reqcur);
                    break;
                }
            }
    
            // prevent infinitely fast rendering ...
            // NOTE: this could be done via a timeout on a _blocking_ dequeue
            // operation above
            wait_for_frame_start(timeout);
        }
    }
    

    The above is a simple version of a thread pool of worker threads. For some background, see my answer: In C, is storing data in local variables in threads akin to creating a local copy? AKA Does this Threadpool synchronization make sense?


    UPDATE:

    Am I correct in assuming that advance_animation_state(reqcur) would increment the request state i.e. request_t->req_state++? And reset it to zero when a single animation loop is done?

    That's what I'd do.

    Or not set it back to zero and use modulo on the req_state in display_animation_frame instead.

    Either way would be okay. But, I'd have display function just render the current state of the animation. Then, have the advance function increment state (i.e. video frame number) until the end. Resetting the state to zero (or via modulo) to provide continuous looping of the animation sequence.

    Separating the animation rendering into display_animation_frame and advance_animation_state is a little more complicated but keeps the system responsive. I currently had the animation rendering in a task that used for-for loop (for the LED matrix) that simply never returned.

    This should be easy enough to split up. The key to the approach is to poll a request queue for new actions at the top of the frame loop (i.e. at the start of each frame).

    This can be split up into smaller functions [usually a good idea] or just add the request queue check to the loop.

    Based on some of the language in the question, you had a monolithic task loop that only did one type of animation:

    void
    render_circle_task(void)
    {
    
        while (1) {
            for (int iframe = 0;  iframe < 1000;  ++iframe) {
                // draw circle to display ...
            }
        }
    }
    

    So, you're trying to split up / restructure something like that.