Difficulty setting up LVGL for monochrome display (STM32, 128x64 display, ST7565 driver, noise/no output)

Goal

I want to render something valid (a button, bar, square, etc) on my display using LVGL so I can begin development.

Tools

  • STM32 Nucleo-F401RE
  • NHD-C12864A1Z display (ST7565 driver)

Bringup Process

  1. Copied over driver code and lv_drv_conf template (modified for my system) from https://github.com/lvgl/lv_drivers/blob/master/display/ST7565.c
  2. Copied lv_port_disp.c/h and modified as follows:
...

#ifndef MY_DISP_HOR_RES
    #define MY_DISP_HOR_RES    128
#endif

#ifndef MY_DISP_VER_RES
    #define MY_DISP_VER_RES    64
#endif

...

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

    static lv_disp_draw_buf_t disp_buf;
    static uint8_t gbuf[ST7565_HOR_RES * ST7565_VER_RES / 8];                          /*A buffer for 10 rows*/
    lv_disp_draw_buf_init(&disp_buf, gbuf, NULL, ST7565_HOR_RES * ST7565_VER_RES / 8);   /*Initialize the display buffer*/

    /*-----------------------------------
     * Register the display in LVGL
     *----------------------------------*/

    static lv_disp_drv_t disp_drv;                         /*Descriptor of a display driver*/
    lv_disp_drv_init(&disp_drv);                    /*Basic initialization*/

    /*Set up the functions to access to your display*/

    /*Set the resolution of the display*/
    disp_drv.hor_res = MY_DISP_HOR_RES;
    disp_drv.ver_res = MY_DISP_VER_RES;

    /*Used to copy the buffer's content to the display*/
    disp_drv.flush_cb = disp_flush;
    disp_drv.rounder_cb = my_rounder_cb;
    disp_drv.set_px_cb = my_set_px_cb;

    /*Set a display buffer*/
    disp_drv.draw_buf = &disp_buf;

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

static void disp_init(void)
{
    st7565_init();
}

...

static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)
{
    if(disp_flush_enabled) {
        st7565_flush(area->x1, area->x2, area->y1, area->y2, color_p);
        lv_disp_flush_ready(disp_drv);
    }
}

void my_rounder_cb(lv_disp_drv_t * disp_drv, lv_area_t * area)
{
  /* Update the areas as needed. Can be only larger.
   * For example to always have lines 8 px height:*/

//	area->x1 = area->x1 & ~(0x7);
//	area->x2 = area->x2 |  (0x7);

   area->y1 = area->y1 & 0x07;
   area->y2 = (area->y2 & 0x07) + 8;
}

void my_set_px_cb(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)
{
    /* Write to the buffer as required for the display.
     * Write only 1-bit for monochrome displays mapped vertically:*/
 buf += buf_w * (y >> 3) + x;
 if(lv_color_brightness(color) > 128) (*buf) |= (1 << (y % 8));
 else (*buf) &= ~(1 << (y % 8));
}
  1. Followed LittlevGL on a Monochrome OLED | LVGL’s Blog and Porting LittlevGL for a monochrome display | by Mattia Maldini | Medium for above functions, along with setting up lv_conf.h. I believe this is the only code I modified/added in the lv_conf.h file:
#define LV_HOR_RES_MAX (128)
#define LV_VER_RES_MAX (64)

/*Color depth: 1 (1 byte per pixel), 8 (RGB332), 16 (RGB565), 32 (ARGB8888)*/
#define LV_COLOR_DEPTH 1

#define LV_COLOR_CHROMA_KEY lv_color_hex(0x00ff00)

#define LV_ANTIALIAS 0
#define LV_USE_ANIMATION 0
#define USE_LV_GPU 0
  1. Added this to main:
  lv_init();
  lv_port_disp_init();
  lv_obj_t * btn = lv_btn_create(lv_scr_act());     /*Add a button the current screen*/
  lv_obj_set_pos(btn, 10, 10);                            /*Set its position*/
  lv_obj_set_size(btn, 40, 20);                          /*Set its size*/

...

  while (1)
  {
	  lv_task_handler();
	  lv_tick_inc(1);
	  HAL_Delay(1);
  }

Problem

My display (tested and working properly using vendor-provided sample code) shows noise/random pixels, but the same pixels every time. I believe I’ve tracked down the problem. The flush function from this driver code that I copied breaks out if y2 < 0. Once I removed the conditional the display started receiving noise, otherwise nothing is sent to the display with the code I currently have. y2 is -1 by the time it reaches this function due to this code block in get_max_row:

if(h_tmp <= 0) {
            LV_LOG_WARN("Can't set draw_buf height using the round function. (Wrong round_cb or to "
                        "small draw_buf)");
            return 0;
        }

Increasing the buffer size (for both the gbuf buffer in my code and the size_in_px_cnt input parameter when initializing lv_disp_draw_buf) from (ST7565_HOR_RES * ST7565_VER_RES / 8) to (ST7565_HOR_RES * ST7565_VER_RES) did not solve the issue.

So far, I’ve managed to track down a bug or two in my code:

  1. st7565_flush was receiving the wrong input parameters when called by disp_flush. It should have been st7565_flush(area->x1, area->y1, area->x2, area->y2, color_p);
  2. The rounder callback may have also been causing problems, but now works properly with:
area->y1 = (area->y1 & (~0x7));
	area->y2 = (area->y2 & (~0x7)) + 7;

At this point I’m getting a black screen with white bars and I’m not sure why (photo attached at the bottom). For some reason, swapping the conditionals in the set_px_cb function so that a color brightness value > 128 sets a pixel white instead of black and vice versa for a value <= 128 gives an all-black screen. After setting a breakpoint at the set_px_cb function, I see that color.full is always true and RGB values are always 255, 255, 255, but the opacity changes (lv_opa_t opa) even though I set LV_COLOR_DEPTH to 1 and the associated code for a monochrome non-grayscale display. Has anyone else experienced a similar problem, or found a solution that may work in my case?