概念先知道
- 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 解码后显示到屏上;两者组合即可实现摄像头实时预览。
操作步骤
本页需要一块 DBI(8080/MIPI DBI)接口 LCD(例程默认 ST7796/ILI9488 等 320×240 屏,实际型号在 lcd_conf_user.h 中配置),并已按接线图连到开发板。若开发板未焊接 LCD 排座,需要外接模块。
在终端进入 SDK 的 JPEG 解码显示例程目录(前提:已按快速开始(Linux)或Windows搭建好环境):
cd examples/tpjdec_disp执行编译命令。Ai-M62(BL616)与 Ai-M61(BL618)同属一个系列,统一填写引脚最少的 bl616 即可:
make CHIP=bl616 BOARD=bl616dk用 USB 线连接开发板,按住 BOOT 键(Ai-M61-32S-Kit 为 IO2)不放、短按 EN/RST 进入下载模式,然后执行烧录(把串口号换成实际值):
make flash CHIP=bl616 COMX=/dev/ttyUSB0打开串口助手(波特率 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_init 按 lcd_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.jpgbuff/btr:读缓冲与请求字节数
返回值:FR_OK(0)成功;其它为 FATFS 错误码
完整代码
以下为 tpjdec_disp/main.c 完整源码,与官方示例一致,默认折叠,点击展开:
📜 点击展开 tpjdec_disp/main.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_FATFS 与 CONFIG_BSP_SDH_SD,把 img0.jpg~img4.jpg 放到 SD 卡根目录 /sd/。

