Esp32 i2s parallel driver with FreeRtos

Baldhead
Posts: 471
Joined: Sun Mar 31, 2019 5:16 am

Esp32 i2s parallel driver with FreeRtos

Postby Baldhead » Mon Mar 02, 2020 7:46 pm

Hi,

I am working in a i2s parallel driver that will use lvgl hmi library, in 2 buffer mode.
Each buffer can store 7680 pixels(1/20 display size).

The i2s parallel driver at firt is working, but i want to put 2 buffer to work synchronously with dma interrupt ie: enable first buffer transfer with dma, while the second buffer are rendered by cpu.
If the cpu finish render second buffer while dma are transfer first buffer yet, the new dma transfer need to wait for dma finish first transfer.
I dont want to polling a flag to know if dma are free or not.
This use cpu for nothing.

The problem are that littlevgl “display_flush” function happens asynchronously and the two buffer need be send synchronously by the driver.

I want to use FreeRtos in some way, but i have many doubts.

I think that my problem can be solved in some way with this:
https://www.freertos.org/RTOS-task-notifications.html

I also need to know if the “dma / i2s io” resource is free or not, but mutex cannot be used within the interrupt subroutine.

I am using esp-idf.

What i want to achieve ?
Using FreeRtos to sync two buffers to be sent by “i2s dma io” driver.
In the future i also want to synchronize the sending buffer with the tearing signal of the display.

Below folow the code of what i want to do:

Code: Select all

//  Main function: Test only without littlevgl yet.
void main( )
{

    lv_area_t area;

    area.x1 = 0x0010;
    area.x2 = 0x013f;    
    area.y1 = 0x0005;    
    area.y2 = 0x01df;   
    
    while(1)
    {
        display_flush ( NULL, &area );   

        // vTaskDelay( 1000 / portTICK_PERIOD_MS );  
    }
}	

void display_flush ( lv_disp_drv_t* drv, lv_area_t* area ) 
{  
    static bool flag = 1;
    flag = !flag;
  
        
    if ( flag == 0 )
    {        
        commands_to_init_pixels_write ( area );    // fills command buffer with some display commands.

        i2s_lcd_write_pixels_a ( (uint32_t) 15360 );          
    }
    else
    {     
        commands_to_init_pixels_write ( area ); 

        i2s_lcd_write_pixels_b ( (uint32_t) 15360 );           
    }
}    


int i2s_lcd_write_pixels_a ( uint32_t length )   // length = bytes number. length <= 15360. 2 bytes per pixel. 
{
    uint32_t i;
    uint8_t* ptr;  

    if ( length > pixels_size_in_bytes )     
    {
        printf( "error: length > pixels_size_in_bytes.\n\n" );
        return -1;
    }     

    ptr = (uint8_t *) &buffer_a;    // buffer_a are lv_color_t(uint16_t) the buffer registered to littlevgl driver.

    for ( i = 0 ; i < length ; i = i + 2 )   
    {        
        buf_a[ i ] = ptr[ i ];                  // buf_a are uint32_t buffer.    
        buf_a[ i + 1 ] = ptr[ i + 1 ];        
    }        
              
    fill_dma_descriptor_a( length );               
    fill_dma_descriptor_command ( 11 );  
    

Are the shared resource i2s0 free ?

If i2s0 is free, blocks access to the i2s peripheral and calls the task "task_send_buffer_a_to_display ()"
to fire the dma transfer. After the dma ends, it releases access to the i2s peripheral within the dma interrupt (i2s is still sending data through the physical port, because of the 64 byte fifo memory).

If i2s0 is not free, the task need waiting here until "dma/i2s" isr subroutine send message(unlock). 
Here the function did not init dma transfer, but need return from this function i think ( confuse ).

    return 2019;
}

////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////
// tasks can be static inline ?


static inline void task_send_buffer_a_to_display( )   
{
    
    i2s_dma_trans_start_interrupt( (uint32_t) &dma_desc_buf_command );
    while(!I2SX.state.tx_idle);  // Wait the peripheral i2s stay free. It is not worth switching tasks on FreeRtos here because sending 11 bytes takes = ~ 1 us and the TICKRATE is around 1 ms - 10 ms.
    
    pixels_flag = 1;   // Informs that it will transfer pixels and not commands. Reset inside "i2s/dma" isr.

    i2s_dma_trans_start_interrupt( (uint32_t) &dma_desc_buf_a[0] );   
}


Same for "int i2s_lcd_write_pixels_b ( uint32_t length )" and for "static inline void task_send_buffer_b_to_display( )" .


static void IRAM_ATTR i2s_isr ( ) 
{   

    if ( I2SX.int_st.out_eof )
    {

        if ( pixels_flag )  // send pixels flag indication.
        {    
            pixels_flag = 0;          
            
            lv_disp_t * disp = lv_refr_get_disp_refreshing();
            lv_disp_flush_ready(&disp->driver);  


            // Release i2s/dma resource. Cannot use mutex inside isr.
            // xTaskNotifyFromISR();  // Notify the task to init dma transfer immediately if the other buffer is already ready to send.  
        }          

    }
   
    I2SX.int_clr.val = I2SX.int_st.val;
}  




Thank's.

Baldhead
Posts: 471
Joined: Sun Mar 31, 2019 5:16 am

Re: Esp32 i2s parallel driver with FreeRtos

Postby Baldhead » Mon Mar 02, 2020 7:48 pm

Now i saw that “lv_disp_flush_ready(&disp->driver)”, should not be called inside the isr because my goal is that the copy of the buffer and the filling of the dma descriptor are ready when starting the next dma transfer.

I think that i must call “lv_disp_flush_ready(&disp->driver)” here:

Code: Select all

  //copy of the buffer
    for ( i = 0 ; i < length ; i = i + 2 )   
    {        
        buf_a[ i ] = ptr[ i ];                  // buf_a are uint32_t buffer.    
        buf_a[ i + 1 ] = ptr[ i + 1 ];        
    }        
              
    fill_dma_descriptor_a( length );       // filling of the dma descriptor for pixels write.               
    fill_dma_descriptor_command ( 11 );    // filling of the dma descriptor for commands write.


    lv_disp_t * disp = lv_refr_get_disp_refreshing();

    if ( i2s_free )    //  i2s free = 1. i2s not free = 0.
    {
        Blocks access to i2s.

        task_send_buffer_a_to_display( );    // start dma transfer   
   
        lv_disp_flush_ready(&disp->driver); 
    }
    else
    {
        Wait for the i2s module to be free( Stay here until dma isr Task Notification ? ).  
        
        Blocks access to i2s.   

        task_send_buffer_a_to_display( );    // start dma transfer

        lv_disp_flush_ready(&disp->driver);
    }

Baldhead
Posts: 471
Joined: Sun Mar 31, 2019 5:16 am

Re: Esp32 i2s parallel driver with FreeRtos

Postby Baldhead » Tue Mar 03, 2020 3:20 am

Any suggestion will be much appreciated.

Thank's.

ESP_Angus
Posts: 2344
Joined: Sun May 08, 2016 4:11 am

Re: Esp32 i2s parallel driver with FreeRtos

Postby ESP_Angus » Wed Mar 11, 2020 12:24 am

Hi Baldhead,

Sorry noone's got back to you with any suggestions. I'm a little unsure from the pseudocode exactly what your approach is here, but I can probably give some pointers.

You're right that you don't want to do too much work in the ISR. Suggest rather than managing the double buffers yourself, you use LittleVGL's built-in double buffering management by passing the two non-screen-sized buffers into lv_disp_buf_init(). This means in the flush callback lvgl will tell you which buffer it wants you to flush to the screen.

The implementation of double buffering in LVGL seems to require that lv_disp_flush_ready() is called after DMA starts sending the active buffer to the hardware, not after the DMA completes. So you don't need to call this from any ISR:
https://github.com/littlevgl/lvgl/blob/ ... isp.c#L270
https://github.com/littlevgl/lvgl/blob/ ... efr.c#L180

You can possibly do something like (pseudocode):

Code: Select all

volatile lldesc_t dma_descriptor; // assuming one is enough to update entire display, maybe you need some other descriptor that holds some command data or something
volatile bool dma_active;

void my_flush_cb(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)
{
  while (dma_active) { } // busy-wait for any existing DMA operation to finish

  dma_descriptor.addr = color_p;
  dma_descriptor.length = length; // calculated from 'area'
  // (may need to do more calcs here or configure a command to send, but this is still probably a small amount of work

  // set I2S address to &dma_descriptor (can do this once in init if you like)  

  // trigger I2s operation to start
  
  dma_active = true;
  
  lv_disp_flush_ready(); // in a double buffering setup, this tells LVGL it can start using the other buffer for graphics updates.
}

void i2s_isr() {
   if (/*this I2S DMA operation is done*/) {
     dma_active = false;
   }
}
This is the simplest version that just spins (busy-waits) on a boolean flag (this is thread-safe as long as there is no read-modify-write of the variable). Possibly you could even spin on an I2S status register instead of a variable, which might eliminate the need for an ISR at all.

You could update the busy-waits to use a task notification if you expect that my_flush_cb() may block for a long time waiting for the previous DMA write to complete. Suggest doing the maths on how long a DMA operation will take to flush 7KB of data, and how frequently you expect LVGL to call flush, as it's possible that it will never block for an extended period - in which case busy-waiting is probably fine and more efficient overall as there is no context switching overhead.

You could make this more complex by having two sets of DMA descriptors, so you can set up the alternate set while the previous DMA operation completes. However setting up the descriptors should take a handful of CPU cycles at most, so suggest you can keep it simple and just update common descriptors while the DMA is idle, before you re-trigger. If any heavy calculation is required to calculate a separate command that sends the range of bytes to update, other command buffers, etc. then you could move the calculations before the busy-wait call and just write out to the descriptors and/or any shared command buffer after DMA is idle.

Sorry I can't give more detail, implementing a custom graphics driver is outside the scope of the support we can really provide, although we may be able to give you a quote for a custom driver.

I assume you've seen the LVGL port that's available in the IoT solution code? This is a working LVGL implementation that may meet your needs.

ESP_houwenxiang
Posts: 118
Joined: Tue Jun 26, 2018 3:09 am

Re: Esp32 i2s parallel driver with FreeRtos

Postby ESP_houwenxiang » Mon Mar 16, 2020 3:36 am

Hi, Baldhead
ESP_Angus provided some good suggestions for you, also you can use queue to make two buffers work synchronously. The code can be like this:

Code: Select all


//DMA idle flag
int dma_idle_flag = 0;
//We used the out_eof interrupt, but the eof interrupt was not generated before the transfer started,
// so we need to start the DMA ourselves for the first transfer.
int trans_set_up_flag = 0;

static inline void task_send_buffer_a_to_display( )   
{
    uint32_t some_thing_from_isr;
    /*
        `some_thing_from_isr` can be a buffer that the CPU can render.
    */
    xQueueReceive(to_task_queue, (void * )&some_thing_from_isr, portMAX_DELAY);
    /*
        do something    
    */
    if(trans_set_up_flag == 1) {
        uint32_t some_thing_to_isr;
        /* `some_thing_to_isr` can be a buffer that has already been rendered.
        */
        xQueueSend(to_isr_queue, &some_thing_to_isr, portMAX_DELAY);
        if(dma_idle_flag == 1) {
            while(!I2SX.state.tx_idle);
            //re-enable out_eof isr
            I2SX.int_ena.out_eof = 1;
        }
    } else {
        trans_set_up_flag = 1;
        /*
            put your code to configure DMA linked list, start DMA transfer.
        */
        I2SX.int_ena.out_eof = 1;
    }
}

static void IRAM_ATTR i2s_isr ( ) 
{   
    portBASE_TYPE high_priority_task_awoken;
    if ( I2SX.int_st.out_eof )
    {
        uint32_t some_thing_from_task;
        //data is not ready
        if(xQueueReceiveFromISR(to_isr_queue, &some_thing_from_task, &high_priority_task_awoken) != pdTRUE) {
            //Disable out_eof interrupt, re-enable in task_send_buffer_a_to_display
            I2SX.int_ena.out_eof = 0;
            dma_idle_flag = 1;
        } else {

            //restart DMA in ISR
            lv_disp_t * disp = lv_refr_get_disp_refreshing();
            lv_disp_flush_ready(&disp->driver);  
            I2SX.int_clr.val = I2SX.int_st.val;
        }
        uint32_t some_thing_to_task;
        xQueueSendFromISR(to_task_queue, (void * )&some_thing_to_task, &high_priority_task_awoken);
    }
        if (high_priority_task_awoken == pdTRUE) {
        portYIELD_FROM_ISR();
    }
} 
wookooho

Baldhead
Posts: 471
Joined: Sun Mar 31, 2019 5:16 am

Re: Esp32 i2s parallel driver with FreeRtos

Postby Baldhead » Thu Apr 02, 2020 12:23 am

Hi ESP_Angus and ESP_houwenxiang,

My driver are working now.
Not the way i would like, but for now i think it's good.

For 5 reasons i think the display driver is not great yet.
I am very perfectionist, but perfectionism requires time and money, and i dont have money yet :).

First reason:
I would like to synchronize the sending buffer with the tearing signal of the display.
Not very simple, because littlevgl update only the objects that change the state on the display(not update all display, if only part of the display changes), this way is required to calculate which line will be updated and calculate the time that the scan of the display will take to reach that line of the display, all after receiving the tearing signal.
Need to consider time of sending and rendering the buffer as well, maybe need reducing the display controller frame rate to 30 Hz or less.

Second reason:
Eliminate buffer copy.
I need copy a uint16_t pixel buffer to the uint32_t dma buffer.
Kisvegabor(littlevgl "owner"), gave me a version of littlevgl, outside the github master, that write 16 bits pixels directly in a uint32_t buffer(dma buffer), but still not tested.

Third reason:
I need to check if the i2s/dma resource is free or not.
Example:
In addition to sending pixels to the display i also need send other commands to the display controller how display_on () or display_off () per example.

A global i2s/dma free resource check with FreeRtos would be much better.
A mutex ?
A mutex with queue ?
I don't know because i am starting to study how a RTOS/FreeRtos work.

Fourth reason:
I'm not sure if using a flag to block the use of i2s/dma resource is the best way to sync the two buffers, being that there is a freeRtos RTOS.
I really don't know how much time is wasted in that loop:
// i2s are free ????
while( i2s_free == 0 ); // i2s are not free.

Fifth reason:
Allocate buffers in specific address at different memory banks.
I read on "esp32_technical_reference_manual_en.pdf", page 31, this:
"Internally, the SRAM is organized in 32K-sized banks. Each CPU and DMA channel can simultaneously access
the SRAM at full speed, provided they access addresses in different memory banks."

ESP32 forum Topic:
viewtopic.php?f=13&t=12905


///////////////////////////////////////////////////////////////////////////////////////////////////////////////////

"You could make this more complex by having two sets of DMA descriptors, so you can set up the alternate set while the previous DMA operation completes. However setting up the descriptors should take a handful of CPU cycles at most, so suggest you can keep it simple and just update common descriptors while the DMA is idle, before you re-trigger. If any heavy calculation is required to calculate a separate command that sends the range of bytes to update, other command buffers, etc. then you could move the calculations before the busy-wait call and just write out to the descriptors and/or any shared command buffer after DMA is idle".

I am doing this, using two dma descriptors for pixels(a and b) and another one dma descriptor for commands, although now i wouldn't even need the command buffer more, i think.
I assemble a packet with commands and pixels in a uint32_t buffer since i need to copy a uint16_t pixel buffer to the uint32_t dma buffer.
I got inspired on an idea that ESP_houwenxiang gave me, that couples the signal data/command in the bit8 of the i2s port.
Pixel data are i2s port bit0, bit1, ..., bit7.
Data/command display signal are i2s port bit8.

The first 11 bytes are display commands and the rest are display pixels.
The first 11 bytes commands i write in another function, before calling this function with buffer copy ( this 11 bytes command buffer calculus the values and filling the buffer are inside display_flush callback function).

"BufferA":

Code: Select all

    ptr = (uint8_t *) &buffer_a;    // buffer_a are uint16_t.

    for ( i = 11, j = 0 ; i < length + 11 ; i = i + 2, j = j + 2 )    // Takes 1,030 ms when length = 15360 bytes.
    {                                                                                                  
        buf_a[ i ]     = ptr[ j ] | 0x000;    // "buf_a[ i ] = ptr[ j ] | 0x000" to set the bit8 pin (i2s module inverted signal) that is used to control the DC signal.
        buf_a[ i + 1 ] = ptr[ j + 1 ] | 0x000;    // "buf_a[ i + 1 ] = ptr[ j + 1 ] | 0x000" to set the bit8 pin (i2s module inverted signal) that is used to control the DC signal.
    }

    fill_dma_descriptor_a( length + 11 );
 
// i2s are free ????           
    while( i2s_free == 0 );    //  i2s are not free. Put timeout maybe here ?????           
                                        //  Wait for the i2s module to be free( Stay here until dma isr Task Notification ? ).  
       
    i2s_free = 0;    // block i2s/dma resource.
    
    i2s_dma_trans_start_interrupt( (uint32_t) &dma_desc_buf_a[0] );    // start dma transfers ( commands and pixels ).   


    disp = lv_refr_get_disp_refreshing( );    
    lv_disp_flush_ready(&disp->driver); 
"BufferB":

Code: Select all

    ptr = (uint8_t *) &buffer_b;    // buffer_b are uint16_t.

    for ( i = 11, j = 0 ; i < length + 11 ; i = i + 2, j = j + 2 )    // Takes 1,030 ms when length = 15360 bytes.
    {                                                                                                  
        buf_b[ i ]     = ptr[ j ] | 0x000;    // "buf_b[ i ] = ptr[ j ] | 0x000" to set the bit8 pin (i2s module inverted signal) that is used to control the DC signal.
        buf_b[ i + 1 ] = ptr[ j + 1 ] | 0x000;    // "buf_b[ i + 1 ] = ptr[ j + 1 ] | 0x000" to set the bit8 pin (i2s module inverted signal) that is used to control the DC signal.
    }

    fill_dma_descriptor_b( length + 11 );
 
// i2s are free ????           
    while( i2s_free == 0 );    //  i2s are not free. Put timeout maybe here ?????           
                                         //  Wait for the i2s module to be free( Stay here until dma isr Task Notification ? ).  
       
    i2s_free = 0;    // block i2s/dma resource.
    
    i2s_dma_trans_start_interrupt( (uint32_t) &dma_desc_buf_b[0] );    // start dma transfers ( commands and pixels ).   


    disp = lv_refr_get_disp_refreshing( );    
    lv_disp_flush_ready(&disp->driver); 
Interrupt subroutine:

Code: Select all

static void IRAM_ATTR i2s_isr ( )    // All enabled i2s module interrupts points here ???? I think so. 
{   
   
    if ( I2SX.int_st.out_eof )       
    {                             
           
        while(!I2SX.state.tx_idle);    // dma interrupt. i2s still sending bytes through the physical bus, because 64 fifo buffer.
 
        I2SX.conf.tx_start = 0;    
        I2SX.conf.tx_reset = 1;        
        I2SX.conf.tx_reset = 0;    

        i2s_free = 1;    // releases the i2s resource.        
    }
    

    if ( I2SX.int_st.out_dscr_err )
    {
        ESP_EARLY_LOGE ( "I2S_TAG", "dma error, interrupt status: 0x%08x", I2SX.int_st.val );  
     
// Needless to say, it is not advised to use printf and other output functions in ISRs. For debugging purposes, use ESP_EARLY_LOGx  
macros when logging from ISRs. Make sure that both TAG and format string are placed into DRAM in that case.
    }
 
 
    I2SX.int_clr.val = I2SX.int_st.val;
}

Who is online

Users browsing this forum: No registered users and 77 guests