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_prepareparses the header andjd_decompdecodes block by block; the callbacksin_func/out_funcread input data and write pixels. - DBI interface: a parallel display bus (8080 timing). BL616/BL618 embed a DBI peripheral (device name
"dbi"), driven here throughlcd_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 keepsCONFIG_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); withCONFIG_FATFSenabled it reads/sd/img0.jpg~img4.jpgfrom 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
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.
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_dispRun the build command. The Ai-M62 (BL616) and Ai-M61 (BL618) belong to the same series, so both use bl616:
make CHIP=bl616 BOARD=bl616dkConnect 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/ttyUSB0Open 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 objectin_func: input callback (reads bytes from the JPEG buffer)work/work_size: work area and its sizedev: session identifier (theIODEVstruct; 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 objectout_func: output callback (copies the decoded rectangle into the framebuffer)scale: scaling factor,0means 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 regionimg: 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 objectpath: path such as/sd/img0.jpgbuff/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
#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/.

