Skip to content

Concepts First

  • JPEG decoding: restoring a compressed JPEG image to raw pixels (RGB565 or YUV444). Decoding is CPU-intensive; this example runs the TJPGD library in PSRAM.
  • TJPGD: a lightweight JPEG decompressor. jd_prepare parses the header and jd_decomp decodes block by block; the callbacks in_func/out_func read input data and write pixels.
  • DBI interface: a parallel display bus (8080 timing). BL616/BL618 embed a DBI peripheral (device name "dbi"), driven here through lcd_init/lcd_draw_picture_nonblocking.
  • PSRAM framebuffer: a 320x240 RGB565 framebuffer needs about 150 KB, so it must live in PSRAM (ATTR_NOINIT_PSRAM_SECTION); the example defconfig keeps CONFIG_PSRAM=y.

Example Overview

This page is based on the tpjdec_disp example in the official Bouffalo SDK (examples/tpjdec_disp), which demonstrates the full JPEG decode + LCD display pipeline:

  • Initializes the DBI LCD (lcd_init), clears the screen, and draws "Hello World !" plus border rectangles;
  • Loads JPEG data: by default 5 built-in test images (img0.h~img4.h); with CONFIG_FATFS enabled it reads /sd/img0.jpg~img4.jpg from an SD card;
  • Decodes with TJPGD (jd_prepare + jd_decomp) and prints image size and decode time;
  • Displays the frame asynchronously (lcd_draw_picture_nonblocking); the registered callback (lcd_async_callback_register) prints a counter when the refresh finishes;
  • Shows each image for 1 second and loops through all 5.

Note

This page complements "MJPEG Video Capture": that page encodes camera frames to JPEG, this one decodes JPEG back to the screen; combining them gives real-time camera preview.

Operation Steps

1
Hardware Preparation

This page needs a DBI (8080/MIPI DBI) interface LCD (default 320x240 panels such as ST7796/ILI9488; set the actual model in lcd_conf_user.h), wired to the board. If your board has no LCD socket, connect an external module.

2
Enter the Example Directory

Open a terminal and enter the SDK JPEG decode & display example directory (prerequisite: set up the environment as in Quick Start (Linux) or Windows):

cd examples/tpjdec_disp
3
Build the Project

Run the build command. The Ai-M62 (BL616) and Ai-M61 (BL618) belong to the same series, so both use bl616:

make CHIP=bl616 BOARD=bl616dk
4
Flash the Firmware

Connect the board with a USB cable, hold the BOOT button (IO2 on the Ai-M61-32S-Kit), briefly press EN/RST to enter download mode, then flash (replace the serial port with the one on your computer):

make flash CHIP=bl616 COMX=/dev/ttyUSB0
5
Run and Verify

Open a serial tool (baud rate 2000000). The LCD first shows “Hello World !” and three colored border rectangles, then cycles one JPEG image per second (the default mode uses 5 built-in test images; with CONFIG_FATFS enabled it reads /sd/img0.jpg~img4.jpg from an SD card). The serial log prints image size and decode time (Image size is 320 x 240., Dec time: xxms).

Code Execution Flow

The full flow from boot to the display loop is:

APIs Used by the Example

jd_prepare(&jdec, in_func, work, work_size, dev)

Parses the JPEG stream header and reports image width/height. in_func is the user-defined input callback that reads data from the buffer.

Parameters:

  • jdec: decompression object
  • in_func: input callback (reads bytes from the JPEG buffer)
  • work / work_size: work area and its size
  • dev: session identifier (the IODEV struct; callbacks use it to access JPEG data)

Returns: JDR_OK (0) on success; other values are error codes

jd_decomp(&jdec, out_func, scale)

Decodes at 1/1 scale; for every decoded block it calls the out_func callback to copy pixels into the framebuffer.

Parameters:

  • jdec: decompression object
  • out_func: output callback (copies the decoded rectangle into the framebuffer)
  • scale: scaling factor, 0 means 1/1

Returns: JDR_OK (0) on success; other values are error codes

lcd_init / lcd_draw_picture_nonblocking(...)

lcd_init initializes the panel selected in lcd_conf_user.h; lcd_draw_picture_nonblocking flushes the framebuffer asynchronously, paired with the callback registered by lcd_async_callback_register.

Parameters:

  • x0/y0/x1/y1: display region
  • img: pixel data pointer

Returns: 0 on success

f_open / f_read / f_close(FATFS APIs, optional)

With CONFIG_FATFS enabled, the example reads /sd/img0.jpg~img4.jpg from an SD card; by default it is disabled and built-in test images are used.

Parameters:

  • fp: file object
  • path: path such as /sd/img0.jpg
  • buff / btr: read buffer and requested byte count

Returns: FR_OK (0) on success; other values are FATFS error codes

Complete Code

The following is the complete source of tpjdec_disp/main.c, identical to the official example, collapsed by default:

📜 Click to expand tpjdec_disp/main.c full code
c

#include "bflb_mtimer.h"
#include "board.h"
#include "lcd.h"
#include "tjpgd.h"

#include "bflb_dbi.h"

/* fatfs */
#ifdef CONFIG_FATFS
#include "fatfs_diskio_register.h"
#include "ff.h"
#else
/* image data */
#include "test_img/img0.h"
#include "test_img/img1.h"
#include "test_img/img2.h"
#include "test_img/img3.h"
#include "test_img/img4.h"
#endif

#define DBG_TAG "MAIN"
#include "log.h"

#if defined(DBI_YUV_SUPPORT) && DBI_YUV_SUPPORT
#define N_BPP 3 /* yuv444 */
#else
#define N_BPP 2 /* rgb565 */
#endif

struct bflb_device_s *dbi_hd;

/* input jpeg buf */
ATTR_NOINIT_PSRAM_SECTION __ALIGNED(64) uint8_t jpeg_buff[32 * 1024];

/* output img buf */
ATTR_NOINIT_PSRAM_SECTION __ALIGNED(64) uint8_t bpp_buff[1][320 * 240 * N_BPP];

/* Session identifier for input/output functions (Name, members and usage are as user defined) */
struct IODEV {
    uint8_t *in_jpg_buf;
    uint32_t in_jpg_buf_size;
    uint32_t in_jpg_data_size;
    uint32_t in_jpg_offset; /* */

    uint8_t *out_img_buf[2]; /* Pointer to the frame buffer */
    uint8_t out_img_buf_index;
    uint32_t out_img_buf_size;
    uint32_t out_img_w; /* Width of the frame buffer [pix] */
    uint32_t out_img_h;
} jpeg_dev;

/*------------------------------*/
/* User defined input funciton  */
/*------------------------------*/
ATTR_PSRAM_CODE_SECTION
size_t in_func(               /* Returns number of bytes read (zero on error) */
               JDEC *jd,      /* Decompression object */
               uint8_t *buff, /* Pointer to the read buffer (null to remove data) */
               size_t nbyte   /* Number of bytes to read/remove */
)
{
    struct IODEV *dev = (struct IODEV *)jd->device; /* Session identifier (5th argument of jd_prepare function) */

    if (buff) {
        /* Raad data from imput stream */
        memcpy(buff, (uint8_t *)dev->in_jpg_buf + dev->in_jpg_offset, nbyte);
    }
    dev->in_jpg_offset += nbyte;

    return nbyte;
}

/*------------------------------*/
/* User defined output funciton */
/*------------------------------*/
ATTR_PSRAM_CODE_SECTION
int out_func(              /* Returns 1 to continue, 0 to abort */
             JDEC *jd,     /* Decompression object */
             void *bitmap, /* Bitmap data to be output */
             JRECT *rect   /* Rectangle region of output image */
)
{
    struct IODEV *dev = (struct IODEV *)jd->device; /* Session identifier (5th argument of jd_prepare function) */

    uint8_t *src, *dst;
    uint16_t y, bws;
    unsigned int bwd;

    /* Progress indicator */
    // if (rect->left == 0) {
    //     LOG_I("\r%lu%%", (rect->top << jd->scale) * 100UL / jd->height);
    // }

    // LOG_I("out rect w:%d, h:%d\r\n", rect->bottom - rect->top + 1, rect->right - rect->left + 1);

    /* Copy the output image rectangle to the frame buffer */
    src = (uint8_t *)bitmap;                                                                            /* Output bitmap */
    dst = dev->out_img_buf[dev->out_img_buf_index] + N_BPP * (rect->top * dev->out_img_w + rect->left); /* Left-top of rectangle in the frame buffer */
    bws = N_BPP * (rect->right - rect->left + 1);                                                       /* Width of the rectangle [byte] */
    bwd = N_BPP * dev->out_img_w;                                                                       /* Width of the frame buffer [byte] */
    for (y = rect->top; y <= rect->bottom; y++) {
        memcpy(dst, src, bws); /* Copy a line */
        src += bws;
        dst += bwd; /* Next line */
    }

    return 1; /* Continue to decompress */
}

/*  */
int jpeg_raw_load(struct IODEV *dev, uint8_t n)
{
    dev->in_jpg_buf = (void *)jpeg_buff;
    dev->in_jpg_buf_size = sizeof(jpeg_buff);
    dev->in_jpg_offset = 0;

#ifdef CONFIG_FATFS
    int ret;
    FIL fnew;
    UINT fnum;
    char file_name[64];

    sprintf(file_name, "/sd/img%d.jpg", n);
    ret = f_open(&fnew, file_name, FA_OPEN_EXISTING | FA_READ);
    if (ret != FR_OK) {
        LOG_E("Fail to open files:%s err:%d\n", file_name, ret);
        return -1;
    }

    if (f_size(&fnew) > dev->in_jpg_buf_size) {
        f_close(&fnew);
        LOG_E("file size over: %d\r\n", f_size(&fnew));
        return -1;
    }

    ret = f_read(&fnew, dev->in_jpg_buf, f_size(&fnew), &fnum);
    LOG_I("file:%s, size:%d, rd_size:%d\r\n", file_name, f_size(&fnew), fnum);
    f_close(&fnew);

#else
    const uint8_t *img_tab[] = { _acimg0, _acimg1, _acimg2, _acimg3, _acimg4 };
    uint32_t img_size_tab[] = { sizeof(_acimg0), sizeof(_acimg1), sizeof(_acimg2), sizeof(_acimg3), sizeof(_acimg4) };

    if (img_size_tab[n] > dev->in_jpg_buf_size) {
        LOG_E("img_data size over: %d\r\n", img_size_tab[n]);
        return -1;
    }

    LOG_I("jpeg_data[%d], size:%d\r\n", n, img_size_tab[n]);

    memcpy(dev->in_jpg_buf, img_tab[n], img_size_tab[n]);
    dev->in_jpg_data_size = img_size_tab[n];

#endif

    return 0;
}

int jpeg_dec(struct IODEV *dev)
{
    int ret;
    JDEC jdec;
    static uint32_t jpg_work_buff[16 * 1024 / 4];

    dev->out_img_buf[0] = bpp_buff[0];
    dev->out_img_buf[1] = bpp_buff[0];
    dev->out_img_buf_size = sizeof(bpp_buff[0]);
    dev->out_img_buf_index = !dev->out_img_buf_index;

    uint32_t start_time = bflb_mtimer_get_time_ms();

    /* prepare jpeg file */
    ret = jd_prepare(&jdec, in_func, (void *)jpg_work_buff, sizeof(jpg_work_buff), dev);
    if (ret != JDR_OK) {
        LOG_E("jd_prepare() failed (rc=%d)\n", ret);
        return -1;
    }

    /* It is ready to dcompress and image info is available here */
    LOG_I("Image size is %u x %u.\r\n", jdec.width, jdec.height);
    LOG_I("%u bytes of work ares is used.\r\n", sizeof(jpg_work_buff) - jdec.sz_pool);

    /* Initialize output device (Create a frame buffer) */
    if (jdec.width > lcd_max_x + 1 || jdec.height > lcd_max_y + 1) {
        LOG_E("Image size OVER\r\n");
    }
    dev->out_img_w = jdec.width;
    dev->out_img_h = jdec.height;

    /* Start to decompress with 1/1 scaling */
    ret = jd_decomp(&jdec, out_func, 0);
    if (ret != JDR_OK) {
        LOG_E("jd_decomp() failed (rc=%d)\n", ret);
        return -1;
    }

    LOG_I("Dec time: %dms\r\n", (uint32_t)(bflb_mtimer_get_time_ms() - start_time));

    return 0;
}

void flush_async_callback()
{
    static uint32_t cnt;
    cnt += 1;

#if defined(DBI_YUV_SUPPORT) && DBI_YUV_SUPPORT
    if (JD_YUV444_MODE) {
        /* Switch YUV444 to RGB565 */
        bflb_dbi_feature_control(dbi_hd, DBI_CMD_INPUT_PIXEL_FORMAT, DBI_PIXEL_INPUT_FORMAT_RGB_565);
    }
    bflb_dbi_feature_control(dbi_hd, DBI_CMD_CLEAR_TX_FIFO, 0);
#endif

    /* lcd async int come */
    LOG_I("lcd async int come, cnt: %d\r\n", cnt);
}

int lcd_disp(struct IODEV *dev)
{
    /* wait flush done */
    while (lcd_draw_is_busy()) {
        LOG_E("lcd_busy\r\n");
    };

#if defined(DBI_YUV_SUPPORT) && DBI_YUV_SUPPORT
    if (JD_YUV444_MODE) {
        /* Switch RGB565 to YUV444 */
        bflb_dbi_feature_control(dbi_hd, DBI_CMD_INPUT_PIXEL_FORMAT, DBI_PIXEL_INPUT_FORMAT_YUV444);
    }
#endif

    LOG_I("disp addr: %p\r\n", (void *)dev->out_img_buf[dev->out_img_buf_index]);

    /* start draw lcd (async) */
    lcd_draw_picture_nonblocking(0, 0, dev->out_img_w - 1, dev->out_img_h - 1, (void *)dev->out_img_buf[dev->out_img_buf_index]);

    return 0;
}

#ifdef CONFIG_FATFS
/* fatfs filesystem init */
int fatfs_init(void)
{
    static FATFS fs;
    FRESULT ret;

    board_sdh_gpio_init();

    fatfs_sdh_driver_register();

    ret = f_mount(&fs, "/sd", 1);
    if (ret != FR_OK) {
        LOG_F("fail to mount filesystem,error= %d\r\n", ret);
        LOG_F("SD card might fail to initialise.\r\n");
        return -1;
    }

    LOG_D("Succeed to mount filesystem\r\n");
    LOG_I("FileSystem cluster size:%d-sectors (%d-Byte)\r\n", fs.csize, fs.csize * 512);
    return 0;
}
#endif

int main(void)
{
    struct IODEV dev;

    board_init();

    LOG_I("tpjdec display test\r\n");

    dbi_hd = bflb_device_get_by_name("dbi");

    /* lcd init */
    lcd_init();
    lcd_set_dir(1, 0);

    LOG_I("LCD init done\r\n");

    /* register async callback */
    lcd_async_callback_register(flush_async_callback);

    /* clean lcd */
    lcd_clear(LCD_COLOR_RGB(0x10, 0x10, 0x10));

    /* disp font */
    lcd_draw_str_ascii16(20, 20, LCD_COLOR_RGB(0xff, 0x00, 0x00), LCD_COLOR_RGB(0x00, 0x00, 0x00), (uint8_t *)"Hello World !", 100);
    lcd_draw_str_ascii16(20, 40, LCD_COLOR_RGB(0x00, 0xff, 0x00), LCD_COLOR_RGB(0x00, 0x00, 0x00), (uint8_t *)"dbi lcd test.", 100);

    lcd_draw_rectangle(0, 0, lcd_max_x, lcd_max_y, LCD_COLOR_RGB(0xff, 0x00, 0x00));
    lcd_draw_rectangle(5, 5, lcd_max_x - 5, lcd_max_y - 5, LCD_COLOR_RGB(0x00, 0xff, 0x00));
    lcd_draw_rectangle(10, 10, lcd_max_x - 10, lcd_max_y - 10, LCD_COLOR_RGB(0x00, 0x00, 0xff));

    bflb_mtimer_delay_ms(500);

#ifdef CONFIG_FATFS
    /* fatfs_init */
    ret = fatfs_init();
    if (ret < 0) {
        while (1) {
        };
    }
#endif

    memset(&jpeg_dev, 0, sizeof(jpeg_dev));

    while (1) {
        for (int i = 0; i < 5; i++) {
            /* load jpeg data */
            jpeg_raw_load(&dev, i);

            /* dec */
            jpeg_dec(&dev);

            /* display */
            lcd_disp(&dev);

            bflb_mtimer_delay_ms(1000);
        }
    }

    return 0;
}

FAQ

The serial log prints \"Image size OVER\" but the program keeps running. Is that normal?

Yes. The example only prints a warning and continues; the out-of-range part is clipped. To avoid it, use images smaller than the LCD resolution, or set the correct panel parameters in lcd_conf_user.h.

Why is PSRAM mandatory?

The JPEG input buffer (32 KB) and the 320x240 RGB565 framebuffer (about 150 KB) are placed in PSRAM sections; on-chip RAM is too small. Your board must have PSRAM (e.g., Ai-M61-32S-Kit / Ai-M62-32S) and keep CONFIG_PSRAM=y in the defconfig.

The LCD is blank or shows garbage. What should I check?

First verify the panel macro at the top of lcd_conf_user.h (default LCD_DBI_ILI9488) matches your panel; then confirm the DBI wiring matches the board; finally make sure CONFIG_BSP_LCD=y is enabled.

How can I display my own images?

Two ways: convert a JPEG to a C array (e.g., with img2hex) and replace the files under test_img/; or enable CONFIG_FATFS and CONFIG_BSP_SDH_SD in the defconfig and put img0.jpg~img4.jpg in the SD card root /sd/.

Released under the MIT License. Build Time 2026-09-11 14:52:23