U8g2: SPI DMA implementation

Created on 8 Sep 2020  路  7Comments  路  Source: olikraus/u8g2

Hi,
Not an issue just looking for more info in what would the best way to implement DMA transmission on my port.
I鈥檓 currently using a LCD with ST75256 (256x160) driver on 4w spi mode on a STM32F3, everything works fine but would like to add DMA to free some resources and optimise the port.

Thanks,
Martin

Most helpful comment

@margce would you mind sharing your code ?

Yes, but please consider that that my implementation is specific for my platform (ST75256 LCD 256x160px, custom STM32F3 board), I've also modified the lower u8x8 routines in favour of more speed. Before I said that the screen refreshes in 5ms but it much less than that.
You might also consider some safe guards in some of the functions to prevent the code getting stuck in some of those while(); i just haven't updated those with some timeouts.

In any case here is how my pin initialisation looks like, notice that DMA_InitStructure is a global variable, I set the fixed config during the setup and and then update the DMA information before starting the transfer.

void u8x8_gpio_init( void ){
    GPIO_InitTypeDef GPIO_InitStructure;
    SPI_InitTypeDef SPI_InitStructure;

    RCC_APB2PeriphResetCmd(LCD_SPI_RCC, ENABLE);
    RCC_APB2PeriphResetCmd(LCD_SPI_RCC, DISABLE);
    RCC_APB2PeriphClockCmd(LCD_SPI_RCC, ENABLE );

    //SCK & MOSI
    GPIO_InitStructure.GPIO_Pin = LCD_SCL_PIN | LCD_MOSI_PIN;
    GPIO_InitStructure.GPIO_Speed = LCD_SCL_SPEED;
    GPIO_InitStructure.GPIO_Mode = LCD_SCL_MODE;
    GPIO_InitStructure.GPIO_OType = LCD_GPIO_OTYPE;
    GPIO_InitStructure.GPIO_PuPd = LCD_GPIO_PUPD;
    GPIO_Init(LCD_GPIO, &GPIO_InitStructure);
    GPIO_PinAFConfig( LCD_GPIO, LCD_SCL_PIN_SOURCE, LCD_SCL_AF );
    GPIO_PinAFConfig( LCD_GPIO, LCD_MOSI_PIN_SOURCE, LCD_MOSI_AF );
    //CD, NSS, RST
    GPIO_InitStructure.GPIO_Pin = LCD_CD_PIN | LCD_NSS_PIN | LCD_RESET_PIN;
    GPIO_InitStructure.GPIO_Mode = LCD_CD_MODE;
    GPIO_Init(LCD_GPIO, &GPIO_InitStructure);

    //SPI config
    SPI_InitStructure.SPI_Direction = SPI_Direction_1Line_Tx;
    SPI_InitStructure.SPI_Mode = SPI_Mode_Master;
    SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;
    SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;
    SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;
    SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;
    SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_2;
    SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;
    SPI_InitStructure.SPI_CRCPolynomial = 7;
    SPI_Init( LCD_SPI, &SPI_InitStructure);

    /*!< Enable the SPI  */
    SPI_Cmd( LCD_SPI, ENABLE );

    /* DMA channel Tx of SPI Configuration */
    DMA_DeInit(LCD_SPI_TX_DMA_CHANNEL);
    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&LCD_SPI_DR;
    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;
    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
    DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
    DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;
    DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
}

Here is how I send a single byte over SPI

void u8x8_spi_byte_send( uint8_t byte )
{
    /*!< Loop while DR register in not empty */
    while((SPI_I2S_GetFlagStatus(LCD_SPI, SPI_I2S_FLAG_TXE) == RESET));
    /*!< Send byte through the SPI peripheral */
    SPI_SendData8(LCD_SPI, byte );
}

Here is my port function that sends several bytes over SPI using DMA, note that we still need to wait for the DMA transfer to be completed before we send the next chunk of u8g2 frame buffer but this transfer is much faster than doing a loop 1 byte at a time.

uint8_t u8x8_dma_spi_port( uint16_t cnt, uint8_t *buff )
{
    /* DMA channel Tx of SPI Configuration */

    LCD_SPI_TX_DMA_CHANNEL->CCR &= (uint16_t)(~DMA_CCR_EN);
    /* Reset DMAy Channelx control register */
    LCD_SPI_TX_DMA_CHANNEL->CCR  = 0;
    /* Reset DMAy Channelx remaining bytes register */
    LCD_SPI_TX_DMA_CHANNEL->CNDTR = 0;
    /* Reset DMAy Channelx peripheral address register */
    LCD_SPI_TX_DMA_CHANNEL->CPAR  = 0;
    /* Reset DMAy Channelx memory address register */
    LCD_SPI_TX_DMA_CHANNEL->CMAR = 0;
    LCD_SPI_DMA->IFCR |= DMA1_CHANNEL3_IT_MASK;

    DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)buff;
    DMA_InitStructure.DMA_BufferSize = cnt;
    DMA_Init(LCD_SPI_TX_DMA_CHANNEL, &DMA_InitStructure);

    /* Enable the SPI Tx DMA request */
    LCD_SPI->CR2 |= LCD_SPI_TX_DMA_REQ;

    /* Enable the DMA channel */
    LCD_SPI_TX_DMA_CHANNEL->CCR |= DMA_CCR_EN;

    // Wait for DMA transfer to complete
    while(DMA_GetFlagStatus(LCD_SPI_TX_DMA_FLAG_TC) == RESET);

    return 1;
}

The DMA transfer is used when drawing the tiles on the screen, what I did was to update my ST75256 driver as shown below, notice that I also used macros to set and reset the CS line in order to avoid having to call the functions that end up just writing 0/1 on that pin. As a reference you can see the original implementation is limit to 248bytes while the DMA can take more because it limited by that limit

    case U8X8_MSG_DISPLAY_DRAW_TILE:
        lcd_cs_set();
        x = ((u8x8_tile_t *) arg_ptr)->x_pos;
        x <<= 3;

        u8x8_cad_SendCmd(u8x8, 0x030); /* select command set */
        u8x8_cad_SendCmd(u8x8, 0x075); /* row */

        /* 0 means flip mode 1 */
        if (u8x8->x_offset == 0) {
            u8x8_cad_SendArg(u8x8, (((u8x8_tile_t *) arg_ptr)->y_pos));
        } else {
            u8x8_cad_SendArg(u8x8, 1 + (((u8x8_tile_t *) arg_ptr)->y_pos));
        }

        u8x8_cad_SendArg(u8x8, 0x04f);
        u8x8_cad_SendCmd(u8x8, 0x015); /* col */
         u8x8_cad_SendArg(u8x8, x+u8x8->x_offset);
        u8x8_cad_SendArg(u8x8, 255);
        u8x8_cad_SendCmd(u8x8, 0x05c);
#if 1
        c = ((u8x8_tile_t *) arg_ptr)->cnt;
        ptr = ((u8x8_tile_t *) arg_ptr)->tile_ptr;
        lcd_cd_set();
        u8x8_dma_spi_port( c<<3, ptr );
#else

        do {
            c = ((u8x8_tile_t *) arg_ptr)->cnt;
            ptr = ((u8x8_tile_t *) arg_ptr)->tile_ptr;
            /* SendData can not handle more than 255 bytes, treat c > 31 correctly  */
            if (c > 31) {
                u8x8_cad_SendData(u8x8, 248, ptr); /* 31*8=248 */
                ptr += 248;
                c -= 31;
            }

            u8x8_cad_SendData(u8x8, c * 8, ptr);
            arg_int--;
        } while (arg_int > 0);
#endif
        lcd_cs_rst();
        return 1;

The other thing that I did was to update the cad logic so setting CD uses a macros instead of calling the port functions U8X8_MSG_BYTE_SET_DC and to send a single byte.

uint8_t u8x8_cad_011(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr)
{
    switch (msg) {
    case U8X8_MSG_CAD_SEND_CMD:
        lcd_cd_rst();
        u8x8_spi_byte_send( arg_int );
        break;
    case U8X8_MSG_CAD_SEND_ARG:
        lcd_cd_set();
        u8x8_spi_byte_send( arg_int );
        break;
    case U8X8_MSG_CAD_SEND_DATA:
        lcd_cd_set();
        //u8x8_byte_SendBytes(u8x8, arg_int, arg_ptr);
        //break;
        /* fall through */
    case U8X8_MSG_CAD_INIT:
    case U8X8_MSG_CAD_START_TRANSFER:
    case U8X8_MSG_CAD_END_TRANSFER:
        return u8x8->byte_cb(u8x8, msg, arg_int, arg_ptr);
    default:
        return 0;
    }

All 7 comments

I assume that this is a none-Arduino environment.

I am not sure how you could save resources with DMA.
Concerning RAM consumption, you should use page mode instead of full buffer mode.
Regarding speed, the first thing should be the implementation of hardware SPI transfer. Details are here:
https://github.com/olikraus/u8g2/wiki/Porting-to-new-MCU-platform#hardware-spi-communication

Unless you use a kind of a double buffer mechanism (which will increase RAM consumption) DMA will not increase speed or reduce code size: You still need to wait until the complete U8g2 buffer is transfered otherwise you run into the risk, that u8g2 overwrites data, which are not yet transfered. As a result, there is no difference between normal SPI transfer and DMA supported SPI transfer. In both cases you need to wait until everything is transfered.

That's correct, I'm running on a custom board and already running on hardware SPI. I'm not concerned about RAM consumption but rather trying to speed up the process of drawing all the tiles.
I understand that I'll need to wait for the DMA transfer to finish but I'm sure there should a speed increase by sending 32 bytes in a single DMA transfer than sending 1 at a time.

From uint8_t u8x8_d_st75256_jlx256160(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr) DRAW_TILE

do { c = ((u8x8_tile_t *) arg_ptr)->cnt; ptr = ((u8x8_tile_t *) arg_ptr)->tile_ptr; /* SendData can not handle more than 255 bytes, treat c > 31 correctly */ if (c > 31) { u8x8_cad_SendData(u8x8, 248, ptr); /* 31*8=248 */ ptr += 248; c -= 31; } u8x8_cad_SendData(u8x8, c * 8, ptr); arg_int--; } while (arg_int > 0);

I think I can optimise that code to set the DC pin and then perform a DMA transfer, couldn't I? I'll try it and measure the speed as well and report back.

I just made a quick test,

  1. Using the standard 4wire SPI hw port it takes about ~16ms to send the entire buffer to the screen.
  2. Updating U8X8_MSG_DISPLAY_DRAW_TILE to use a SPI DMA transfer and optimising calls to set/reset CD pin it reduced the transfer time to under 5ms!

I understand that these modifications are very specific to my port but i think it's worth it in my case.

Thanks!
Martin

Well, sounds good.

@margce would you mind sharing your code ?

@margce would you mind sharing your code ?

Yes, but please consider that that my implementation is specific for my platform (ST75256 LCD 256x160px, custom STM32F3 board), I've also modified the lower u8x8 routines in favour of more speed. Before I said that the screen refreshes in 5ms but it much less than that.
You might also consider some safe guards in some of the functions to prevent the code getting stuck in some of those while(); i just haven't updated those with some timeouts.

In any case here is how my pin initialisation looks like, notice that DMA_InitStructure is a global variable, I set the fixed config during the setup and and then update the DMA information before starting the transfer.

void u8x8_gpio_init( void ){
    GPIO_InitTypeDef GPIO_InitStructure;
    SPI_InitTypeDef SPI_InitStructure;

    RCC_APB2PeriphResetCmd(LCD_SPI_RCC, ENABLE);
    RCC_APB2PeriphResetCmd(LCD_SPI_RCC, DISABLE);
    RCC_APB2PeriphClockCmd(LCD_SPI_RCC, ENABLE );

    //SCK & MOSI
    GPIO_InitStructure.GPIO_Pin = LCD_SCL_PIN | LCD_MOSI_PIN;
    GPIO_InitStructure.GPIO_Speed = LCD_SCL_SPEED;
    GPIO_InitStructure.GPIO_Mode = LCD_SCL_MODE;
    GPIO_InitStructure.GPIO_OType = LCD_GPIO_OTYPE;
    GPIO_InitStructure.GPIO_PuPd = LCD_GPIO_PUPD;
    GPIO_Init(LCD_GPIO, &GPIO_InitStructure);
    GPIO_PinAFConfig( LCD_GPIO, LCD_SCL_PIN_SOURCE, LCD_SCL_AF );
    GPIO_PinAFConfig( LCD_GPIO, LCD_MOSI_PIN_SOURCE, LCD_MOSI_AF );
    //CD, NSS, RST
    GPIO_InitStructure.GPIO_Pin = LCD_CD_PIN | LCD_NSS_PIN | LCD_RESET_PIN;
    GPIO_InitStructure.GPIO_Mode = LCD_CD_MODE;
    GPIO_Init(LCD_GPIO, &GPIO_InitStructure);

    //SPI config
    SPI_InitStructure.SPI_Direction = SPI_Direction_1Line_Tx;
    SPI_InitStructure.SPI_Mode = SPI_Mode_Master;
    SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;
    SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;
    SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;
    SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;
    SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_2;
    SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;
    SPI_InitStructure.SPI_CRCPolynomial = 7;
    SPI_Init( LCD_SPI, &SPI_InitStructure);

    /*!< Enable the SPI  */
    SPI_Cmd( LCD_SPI, ENABLE );

    /* DMA channel Tx of SPI Configuration */
    DMA_DeInit(LCD_SPI_TX_DMA_CHANNEL);
    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&LCD_SPI_DR;
    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;
    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
    DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
    DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;
    DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
}

Here is how I send a single byte over SPI

void u8x8_spi_byte_send( uint8_t byte )
{
    /*!< Loop while DR register in not empty */
    while((SPI_I2S_GetFlagStatus(LCD_SPI, SPI_I2S_FLAG_TXE) == RESET));
    /*!< Send byte through the SPI peripheral */
    SPI_SendData8(LCD_SPI, byte );
}

Here is my port function that sends several bytes over SPI using DMA, note that we still need to wait for the DMA transfer to be completed before we send the next chunk of u8g2 frame buffer but this transfer is much faster than doing a loop 1 byte at a time.

uint8_t u8x8_dma_spi_port( uint16_t cnt, uint8_t *buff )
{
    /* DMA channel Tx of SPI Configuration */

    LCD_SPI_TX_DMA_CHANNEL->CCR &= (uint16_t)(~DMA_CCR_EN);
    /* Reset DMAy Channelx control register */
    LCD_SPI_TX_DMA_CHANNEL->CCR  = 0;
    /* Reset DMAy Channelx remaining bytes register */
    LCD_SPI_TX_DMA_CHANNEL->CNDTR = 0;
    /* Reset DMAy Channelx peripheral address register */
    LCD_SPI_TX_DMA_CHANNEL->CPAR  = 0;
    /* Reset DMAy Channelx memory address register */
    LCD_SPI_TX_DMA_CHANNEL->CMAR = 0;
    LCD_SPI_DMA->IFCR |= DMA1_CHANNEL3_IT_MASK;

    DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)buff;
    DMA_InitStructure.DMA_BufferSize = cnt;
    DMA_Init(LCD_SPI_TX_DMA_CHANNEL, &DMA_InitStructure);

    /* Enable the SPI Tx DMA request */
    LCD_SPI->CR2 |= LCD_SPI_TX_DMA_REQ;

    /* Enable the DMA channel */
    LCD_SPI_TX_DMA_CHANNEL->CCR |= DMA_CCR_EN;

    // Wait for DMA transfer to complete
    while(DMA_GetFlagStatus(LCD_SPI_TX_DMA_FLAG_TC) == RESET);

    return 1;
}

The DMA transfer is used when drawing the tiles on the screen, what I did was to update my ST75256 driver as shown below, notice that I also used macros to set and reset the CS line in order to avoid having to call the functions that end up just writing 0/1 on that pin. As a reference you can see the original implementation is limit to 248bytes while the DMA can take more because it limited by that limit

    case U8X8_MSG_DISPLAY_DRAW_TILE:
        lcd_cs_set();
        x = ((u8x8_tile_t *) arg_ptr)->x_pos;
        x <<= 3;

        u8x8_cad_SendCmd(u8x8, 0x030); /* select command set */
        u8x8_cad_SendCmd(u8x8, 0x075); /* row */

        /* 0 means flip mode 1 */
        if (u8x8->x_offset == 0) {
            u8x8_cad_SendArg(u8x8, (((u8x8_tile_t *) arg_ptr)->y_pos));
        } else {
            u8x8_cad_SendArg(u8x8, 1 + (((u8x8_tile_t *) arg_ptr)->y_pos));
        }

        u8x8_cad_SendArg(u8x8, 0x04f);
        u8x8_cad_SendCmd(u8x8, 0x015); /* col */
         u8x8_cad_SendArg(u8x8, x+u8x8->x_offset);
        u8x8_cad_SendArg(u8x8, 255);
        u8x8_cad_SendCmd(u8x8, 0x05c);
#if 1
        c = ((u8x8_tile_t *) arg_ptr)->cnt;
        ptr = ((u8x8_tile_t *) arg_ptr)->tile_ptr;
        lcd_cd_set();
        u8x8_dma_spi_port( c<<3, ptr );
#else

        do {
            c = ((u8x8_tile_t *) arg_ptr)->cnt;
            ptr = ((u8x8_tile_t *) arg_ptr)->tile_ptr;
            /* SendData can not handle more than 255 bytes, treat c > 31 correctly  */
            if (c > 31) {
                u8x8_cad_SendData(u8x8, 248, ptr); /* 31*8=248 */
                ptr += 248;
                c -= 31;
            }

            u8x8_cad_SendData(u8x8, c * 8, ptr);
            arg_int--;
        } while (arg_int > 0);
#endif
        lcd_cs_rst();
        return 1;

The other thing that I did was to update the cad logic so setting CD uses a macros instead of calling the port functions U8X8_MSG_BYTE_SET_DC and to send a single byte.

uint8_t u8x8_cad_011(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr)
{
    switch (msg) {
    case U8X8_MSG_CAD_SEND_CMD:
        lcd_cd_rst();
        u8x8_spi_byte_send( arg_int );
        break;
    case U8X8_MSG_CAD_SEND_ARG:
        lcd_cd_set();
        u8x8_spi_byte_send( arg_int );
        break;
    case U8X8_MSG_CAD_SEND_DATA:
        lcd_cd_set();
        //u8x8_byte_SendBytes(u8x8, arg_int, arg_ptr);
        //break;
        /* fall through */
    case U8X8_MSG_CAD_INIT:
    case U8X8_MSG_CAD_START_TRANSFER:
    case U8X8_MSG_CAD_END_TRANSFER:
        return u8x8->byte_cb(u8x8, msg, arg_int, arg_ptr);
    default:
        return 0;
    }

Wow thank you so much, I will try to implement it for SSD1306!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

idreamsi picture idreamsi  路  5Comments

WaRZaT picture WaRZaT  路  3Comments

ribasco picture ribasco  路  5Comments

sarwarislammoon picture sarwarislammoon  路  4Comments

idreamsi picture idreamsi  路  7Comments