Implementing flush_cb with HAL_SPI_Transmit for SSD1306

Description

Help please to use LVGL with a SSD1306 OLED and STM32 HAL.

MCU board and compiler

STM32F405RGT MCU
Custom board with SPI1 pins
Similar to a Nucleo-F401RE
Compiler: gcc-arm(1)
Build system: GNU Make
Libs: STM32 HAL
LVGL: Release/v7
Editor: STM32CubeMX

SPI connection (MCU to OLED)

MCU pin SPI function
PA4 (Pin-20) Chip select (CS)
PA5 (Pin-21) SPI1 SCK (D0)
PA7 (Pin-23) SPI1 MOSI (D1)
PB0 (Pin-26) Data command (DC)
PB1 (Pin-27) Reset (RES)
3V3
GND

What do you want to achieve?

I want to display ‘Hello World’ to a monochrome OLED SSD1306 using SPI without DMA.

Please tell me how to use HAL_SPI_Transmit below in the flush_cb where it states ‘Put a pixel to the display.’

What have you tried so far?

I implemented flush_cb() according to outdated tutorials not using STM32 nor SPI, so my lvgl integration is not working (nothing is displayed.)

Code to reproduce

Initialisation in main.c

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */
  lv_init();
  lv_port_disp_init();
  lv_theme_mono_init(LV_COLOR_BLACK, LV_COLOR_WHITE, 0, NULL, NULL, NULL, NULL);
  lv_ex_get_started_1();
  /* USER CODE END SysInit */

My flush callback lacking HAL_SPI

static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)
{
    int32_t x;
    int32_t y;
    for(y = area->y1; y <= area->y2; y++) {
        for(x = area->x1; x <= area->x2; x++) {
            /* Put a pixel to the display. For example: */
            /* put_px(x, y, *color_p)*/
            color_p++;
        }
    }
    lv_disp_flush_ready(disp_drv);
}

The entire lv_port_disp.c file

/*********************
 *      INCLUDES
 *********************/
#include "lv_port_disp.h"

/*********************
 *      DEFINES
 *********************/
#define DISP_BUF_SIZE (LV_HOR_RES_MAX*LV_VER_RES_MAX/8)
#define BIT_SET(a,b) ((a) |= (1U<<(b)))
#define BIT_CLEAR(a,b) ((a) &= ~(1U<<(b)))

/**********************
 *  STATIC PROTOTYPES
 **********************/
static void disp_init(void);

static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p);
static void disp_round(lv_disp_drv_t * disp_drv, lv_area_t * area);
static void disp_setpx(lv_disp_drv_t * disp_drv, uint8_t * buf, lv_coord_t buf_w, lv_coord_t x, lv_coord_t y, lv_color_t color, lv_opa_t opa);
#if LV_USE_GPU
static void gpu_blend(lv_disp_drv_t * disp_drv, lv_color_t * dest, const lv_color_t * src, uint32_t length, lv_opa_t opa);
static void gpu_fill(lv_disp_drv_t * disp_drv, lv_color_t * dest_buf, lv_coord_t dest_width, const lv_area_t * fill_area, lv_color_t color);
#endif

/**********************
 *   GLOBAL FUNCTIONS
 **********************/

void lv_port_disp_init(void)
{
    /*-------------------------
     * Initialize your display
     * -----------------------*/
    disp_init();

    // Set a display buffer of the whole screen
    static lv_disp_buf_t disp_buf;          // Descriptor of display buffer
    static lv_color_t gbuf[DISP_BUF_SIZE];  // Memory used as display buffer
    lv_disp_buf_init(&disp_buf, gbuf, NULL, DISP_BUF_SIZE);

    /*-----------------------------------
     * Register the display in LVGL
     *----------------------------------*/
    lv_disp_drv_t disp_drv;       /*Descriptor of a display driver*/
    lv_disp_drv_init(&disp_drv);  /*Basic initialization*/

    /*Set the resolution of the display*/
    disp_drv.hor_res = LV_HOR_RES_MAX;
    disp_drv.ver_res = LV_VER_RES_MAX;

    /*Callbacks to copy buffer contents to the display*/
    disp_drv.flush_cb = disp_flush;
    disp_drv.rounder_cb = disp_round;
    disp_drv.set_px_cb = disp_setpx;
    disp_drv.sw_rotate = 0;
    disp_drv.rotated = LV_DISP_ROT_NONE;  // LV_DISP_ROT_180

    /*Set a display buffer*/
    disp_drv.buffer = &gbuf;

    /*Finally register the driver*/
    lv_disp_drv_register(&disp_drv);
}

/**********************
 *   STATIC FUNCTIONS
 **********************/

/* Initialize your display and the required peripherals. */
static void disp_init(void)
{
    /*You code here*/
}

static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)
{
    int32_t x;
    int32_t y;
    for(y = area->y1; y <= area->y2; y++) {
        for(x = area->x1; x <= area->x2; x++) {
            /* Put a pixel to the display. For example: */
            /* put_px(x, y, *color_p)*/
            color_p++;
        }
    }
    lv_disp_flush_ready(disp_drv);
}

static void disp_round(lv_disp_drv_t *disp_drv, lv_area_t *area) {
  area->y1 = (area->y1 & (~0x7));
  area->y2 = (area->y2 & (~0x7)) + 7;
}

static void disp_setpx(lv_disp_drv_t *disp_drv, uint8_t *buf, lv_coord_t buf_w, lv_coord_t x, lv_coord_t y, lv_color_t color, lv_opa_t opa) {
  uint16_t byte_index = x + ((y >> 3) * buf_w);
  uint8_t  bit_index  = y & 0x7;
  if (color.full == 0) {  // == 0 inverts
    BIT_SET(buf[byte_index], bit_index);
  }
  else {
    BIT_CLEAR(buf[byte_index], bit_index);
  }
}

Screenshot

Device photograph

STM32CubeMX GPIO screenshot

STM32CubeMX SPI1 screenshot

Thanks!

I’m really grateful to get started with LVGL, thanks for the help.

I looked in all the branches of lv_drivers for SSD1306 or similar but only found display/SSD1963.c which is strange, consisting mostly of driver IC commands I guess?

There were some suboptimal MCU clock settings and the delay_uc(3) was poorly implemented (for 5us delays in between reset line changes.) The worst bug however, was in lv_port_disp_init(3):

< /*Set a display buffer*/
< disp_drv.buffer = &gbuf;
---
> /*Set a display buffer*/
> disp_drv.buffer = &disp_buf;

Here are the missing lines from my lv_port_disp source for SSD1306 monochrome LEDs:

static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)
{
  uint8_t nRow1 = area->y1>>3;
  uint8_t nRow2 = area->y2>>3;
  uint8_t *pBuf = (uint8_t*)color_p;

  for(uint8_t nRow = nRow1; nRow <= nRow2; nRow++) {
    uint8_t flush_cmds[] = {
      0xB0 | nRow,
      OLED_SETLOWCOL | (area->x1 & 0xF),
      OLED_SETHIGHCOL | ((area->x1>>4) & 0xF)
    };
    oled_cmd(flush_cmds, sizeof(flush_cmds));
    HAL_GPIO_WritePin(OLED_CS_GPIO_Port, OLED_CS_Pin, GPIO_PIN_RESET);
    HAL_GPIO_WritePin(OLED_DC_GPIO_Port, OLED_DC_Pin, GPIO_PIN_RESET);
    HAL_SPI_Transmit(&hspi1, flush_cmds, sizeof(flush_cmds), SPI_TIMEOUT_MAX);
    HAL_GPIO_WritePin(OLED_CS_GPIO_Port, OLED_CS_Pin, GPIO_PIN_SET);

    for (uint16_t nCol = area->x1; nCol <= area->x2; nCol++) {
      HAL_GPIO_WritePin(OLED_CS_GPIO_Port, OLED_CS_Pin, GPIO_PIN_RESET);
      HAL_GPIO_WritePin(OLED_DC_GPIO_Port, OLED_DC_Pin, GPIO_PIN_SET);
      HAL_SPI_Transmit(&hspi1, pBuf, sizeof(*pBuf), SPI_TIMEOUT_MAX);
      HAL_GPIO_WritePin(OLED_CS_GPIO_Port, OLED_CS_Pin, GPIO_PIN_SET);
      pBuf++;
    }

  lv_disp_flush_ready(disp_drv);
}

I spent about one week trying to get LittleVGL working on my custom STM32F405RGT device with a SSD1306 128x64 monochrome OLED, using HAL calls, SPI, and without DMA. It works now.

Feel free to use this information when correcting the missing content in the page Get Started/STM32.

Thanks to everyone for all the help!

Cheers,
Michael