Skip to content

概念先知道

  • JPEG 解码:把压缩的 JPEG 图片还原成像素点阵(RGB565 或 YUV444)。解码是 CPU 密集操作,例程用 TJPGD 库在 PSRAM 中完成。
  • TJPGD:一个轻量级 JPEG 解码库(Tiny JPEG Decompressor),通过 jd_prepare 解析头、jd_decomp 逐块解码,回调函数 in_func/out_func 负责读数据、写像素。
  • DBI 接口:显示屏并行接口(8080 时序),BL616/BL618 内置 DBI 外设(设备名 "dbi"),例程用 lcd_init/lcd_draw_picture_nonblocking 驱动。
  • PSRAM 帧缓冲:320×240 的 RGB565 帧缓冲约 150KB,必须放在 PSRAM(ATTR_NOINIT_PSRAM_SECTION),所以例程 defconfig 开启 CONFIG_PSRAM=y

例程功能简介

本页对应博流官方 SDK 的 tpjdec_disp 例程(examples/tpjdec_disp),演示 JPEG 解码 + LCD 显示的完整链路:

  • 初始化 DBI LCD(lcd_init),清屏并绘制 “Hello World !” 文字与边框矩形;
  • 加载 JPEG 数据:默认模式使用内置 5 张测试图(img0.h~img4.h);开启 CONFIG_FATFS 后从 SD 卡读取 /sd/img0.jpg~img4.jpg
  • 用 TJPGD 解码(jd_prepare + jd_decomp),打印图片尺寸与解码耗时;
  • lcd_draw_picture_nonblocking 异步显示到 LCD,注册的异步回调(lcd_async_callback_register)在刷新完成时打印计数;
  • 每张图显示 1 秒,5 张循环播放。

提示

本页与「MJPEG 视频采集」互补:那页把摄像头画面编码成 JPEG,本页把 JPEG 解码后显示到屏上;两者组合即可实现摄像头实时预览。

操作步骤

1
硬件准备

本页需要一块 DBI(8080/MIPI DBI)接口 LCD(例程默认 ST7796/ILI9488 等 320×240 屏,实际型号在 lcd_conf_user.h 中配置),并已按接线图连到开发板。若开发板未焊接 LCD 排座,需要外接模块。

2
进入例程目录

在终端进入 SDK 的 JPEG 解码显示例程目录(前提:已按快速开始(Linux)Windows搭建好环境):

cd examples/tpjdec_disp
3
编译工程

执行编译命令。Ai-M62(BL616)与 Ai-M61(BL618)同属一个系列,统一填写引脚最少的 bl616 即可:

make CHIP=bl616 BOARD=bl616dk
4
烧录固件

用 USB 线连接开发板,按住 BOOT 键(Ai-M61-32S-Kit 为 IO2)不放、短按 EN/RST 进入下载模式,然后执行烧录(把串口号换成实际值):

make flash CHIP=bl616 COMX=/dev/ttyUSB0
5
运行验证

打开串口助手(波特率 2000000)。LCD 先显示 “Hello World !” 与三个彩色边框矩形,随后每 1 秒切换一张 JPEG 图片(默认模式用内置 5 张测试图;若在 defconfig 开启 CONFIG_FATFS 则从 SD 卡读取 /sd/img0.jpg~img4.jpg)。串口会打印图片尺寸与解码耗时(Image size is 320 x 240.Dec time: xxms)。

代码执行流程

例程从启动到循环显示的完整流程如下:

例程调用的 API 介绍

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

解析 JPEG 数据流头信息,返回图片宽高。in_func 是用户定义的输入回调,负责从缓冲区读取数据。

参数

  • jdec:解码器对象
  • in_func:输入回调(从 JPEG 缓冲区读字节)
  • work / work_size:工作区与大小
  • dev:会话标识(例程传入 IODEV,回调里用它拿 JPEG 数据)

返回值JDR_OK(0)成功;其它为错误码

jd_decomp(&jdec, out_func, scale)

按 1/1 缩放执行解码,每解出一块就调用 out_func 回调,把像素写入帧缓冲。

参数

  • jdec:解码器对象
  • out_func:输出回调(把解码矩形写入帧缓冲)
  • scale:缩放比,0 表示 1/1

返回值JDR_OK(0)成功;其它为错误码

lcd_init / lcd_draw_picture_nonblocking(...)

lcd_initlcd_conf_user.h 初始化所选屏幕;lcd_draw_picture_nonblocking 把帧缓冲异步刷到 LCD,配合 lcd_async_callback_register 注册的完成回调使用。

参数

  • x0/y0/x1/y1:显示区域
  • img:像素数据指针

返回值0 成功

f_open / f_read / f_close(FATFS API,可选)

开启 CONFIG_FATFS 时,例程用 FATFS 从 SD 卡读取 /sd/img0.jpg~img4.jpg;默认关闭,改用内置测试图数组。

参数

  • fp:文件对象
  • path:路径,如 /sd/img0.jpg
  • buff / btr:读缓冲与请求字节数

返回值FR_OK(0)成功;其它为 FATFS 错误码

完整代码

以下为 tpjdec_disp/main.c 完整源码,与官方示例一致,默认折叠,点击展开:

📜 点击展开 tpjdec_disp/main.c 完整代码
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

串口打印 “Image size OVER” 但程序没停,正常吗?

正常。例程只打印警告不退出,超出的部分会被裁掉显示。要避免,请换用小于 LCD 分辨率的图片,或在 lcd_conf_user.h 中把屏幕参数配成实际型号。

为什么必须开 PSRAM?

JPEG 输入缓冲(32KB)与 320×240 RGB565 帧缓冲(约 150KB)都放在 PSRAM 段,片上 RAM 不够放。开发板必须带 PSRAM(Ai-M61-32S-Kit / Ai-M62-32S 等),并保持 defconfig 中 CONFIG_PSRAM=y

LCD 花屏或不亮怎么办?

先核对 lcd_conf_user.h 顶部的屏幕型号宏(默认 LCD_DBI_ILI9488)与实际屏一致;再确认 DBI 引脚接线与开发板对应;最后检查 CONFIG_BSP_LCD=y 已开启。

想显示自己的图片怎么办?

两种方式:把 JPEG 转成 C 数组(如 img2hex 工具)替换 test_img/ 下的头文件;或在 defconfig 开启 CONFIG_FATFSCONFIG_BSP_SDH_SD,把 img0.jpg~img4.jpg 放到 SD 卡根目录 /sd/

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