使用轻量级图像解码器TJpgDec - 基于MM32F5微控制器和MindSDK

使用轻量级图像解码器TJpgDec - 基于MM32F5微控制器和MindSDK

苏勇,[email protected],2022年12月

文章目录

  • 使用轻量级图像解码器TJpgDec - 基于MM32F5微控制器和MindSDK
    • 缘起
    • TJpgDec简介
      • jd_prepare() - 准备解码JPEG图像
      • jd_decomp() - 执行解码JPEG图像
      • tjpgdcnf.h - 配置文件
      • 关于TJpgDec的软件许可证
    • 应用接口解析
      • in_func() - 输入数据流回调函数
      • out_func() - 输出数据流回调函数
      • 关于工作区和帧缓冲区
    • 在MM32F5微控制器上应用
    • 一点思考
    • 参考文献

缘起

移植TJpgDec的文章在我的todo list里躺了快3年,最近关于图像解码的设计需求又被列入到当前的项目当中,趁着这个机会赶紧把移植过程记录下来。隐约记得最早在nxp lpc5500平台上适配TJpgDec的时候遇到过一些问题,并通过研读程序源码找到了解决方案。时至今日,印象逐渐消散,但又不舍得忘却,希望尽早记录下来,给大脑减负。

TJpgDec是难得可以运行在小资源微控制器的解码图片组件,对系统资源要求很少,虽然在作者提供的原版样例工程中使用了动态内存分配的函数(malloc和free),但经过调整源码可以使用静态内存取代动态内存的用法,比较容易集成在微控制器平台的软件中。

编解码这种对算法有一定要求的组件,对开发者的理论水平有相当的要求,同通信类的协议栈更多处理通信交互行为不同,对普通的嵌入式软件开发者有相当高的门槛,自己硬编码一般搞不定。所以,如果开发者能玩一遍TJpgDec,积累一个jpeg图像解码的工具,解锁一项关于多媒体开发的技能,这无疑是一件令人心安的事情。

偶然发现TJpgDec,源头还是多年前捣鼓FatFs文件系统时发现了同一个作者搞了Petty FatFs(一个FatFs小资源版本),通过网站域名找到原作者的主页上向看看还有什么有趣的软件,其实我对这种在小资源微控制器上实现的功能组件还是非常感兴趣的。结果发现作者ChaN也是一个2B青年,他的项目主页(http://www.elm-chan.org/)竟然是二次元风格的。如图x所示。

图x ChaN的项目网站主页

其中软件分类的页面(http://www.elm-chan.org/fsw_e.html)中罗列了ChaN在持续维护的若干项目,除了文件系统组件之外,原本期望可能会有一个基于文件系统的shell,结果shell软件没找到,倒是找到了一个JPEG图像解码器。如图x所示。

使用轻量级图像解码器TJpgDec - 基于MM32F5微控制器和MindSDK_第1张图片

图x ChaN软件项目清单的网页

从网页上看,ChaN最近又在微控制器平台上搞了一些新的软件,例如Clopboard Saver。对FatFs和TJpgDec软件也进行了更新。当时看到TJpgDec的项目简介中专门提到专为小型嵌入式系统进行了高度优化,嗯,有熟悉的味道,就点进TJpgDec的项目主页(http://www.elm-chan.org/fsw/tjpgd/00index.html)里看了看。果不奇然,同FatFs类似的简单明了的风格,嗯,看起来是个不错的软件。因为刚好手头上在调的板子上有个屏,就花了一些时间就调了起来,真香。TJpgDec的项目主页,如图x所示。

使用轻量级图像解码器TJpgDec - 基于MM32F5微控制器和MindSDK_第2张图片

图x TJpgDec项目网站主页

本文简单介绍了TJpgDec组件的情况,以及汇总了关于TJpgDec组件的所有必要的资料,然后对原作者描述的移植接口进行了详细的解释,补充一部分原作者在网页中简要带过的内容,最后在使用国产灵动微电子的MM32F5277E9P(Arm Cortex-MC1处理器)的plus-f5270开发板上进行具体移植,使用静态内存分配的方式换掉动态内存的用法。这个样例工程已经加入到灵动微电子官方的软件开发平台MindSDK的开发分支,即将在下一次release将面向公众发布。

TJpgDec简介

TJpgDec是一个为小型嵌入式系统高度优化的创建JPEG图像的解码软件。它工作时占用的内存非常低,以便它可以集成到微控芯片,如AVR, 8051, PIC, Z80, Cortex-M0等。不依赖于具体的硬件平台,使用ANSI-C编写,易于使用的操作模式,完全可重入的体系结构,非常小的内存占用(独立于图像尺寸的3KB SRAM。3.5-8.5KB的代码空间)。输出格式可以是缩放比例为1/1、1/2、1/4或1/8可选,像素格式可以是RGB888或RGB565。

TJpgDec组件是一个免费开放的软件,可用于教育、科研和商业开发。用户可以在包含TJpgDec的个人项目和商业产品中改写和重新发布该组件。

TJpgDec组件在用户的应用中仅需要调用2个API。

jd_prepare() - 准备解码JPEG图像

jd_prepare 分析JPEG数据并创建一个解码对象用于随后的解码过程。

jd_prepare函数是JPEG解码过程的第一阶段。它接收用户传入的数据流(需要通过传入的回调函数操作数据流),分析JPEG图像和创建解码参数表。函数成功后,会话准备好在jd_decomp函数解码JPEG图像。应用程序可以参考JPEG解码对象中存储的尺寸大小。这个信息将用于在后续的解码阶段配置输出设备和参数。

JRESULT jd_prepare (
			JDEC* jdec,            /* Pointer to blank decompression object */
			UINT(*infunc)(JDEC*,BYTE*,UINT), /* Pointer to input function */
			void* work,            /* Pointer to work area */
			UINT sz_work,          /* Size of the work area */
			void* device           /* Device identifier for the session */
			);

输入参数:

  • jdec - 输出,一个空的JDEC结构体对象,初始化一个解码对象。需要用户先创建JPEC对象的内存,然后本函数在执行过程中会向其中填充有效信息,这个解码对象也将用于后续的解码操作。
  • infunc - 输入,指定一个为解码算法提供输入数据的回调函数。实际上,这个方法也是关联具体平台的,不仅仅是提供一个读数的方法,更确切是提供一个具体的数据流。这个infunc函数也是需要在具体平台适配过程中需要用户自行实现的。具体写法可以看原作者给的应用说明,也可以参见本文中的基于具体微控制器平台的实现。
  • work - 输入,指定一个内存块,给解码器内部运行算法的工作空间。
  • sz_work - 输入,指定work参数指定工作空间的 size,以字节为单位。TJpgDec至多需要3092字节的工作区域,这依赖于JPEG图像的内置参数表。通常情况下是3092字节工作区域。
  • device - 输入,指定用户自定义的会话设备标识。它保存在解码对象jdec的字段device中。它可以用于I/O函数去识别当前会话。当I/O device固定在project或者不需要这个功能,设置为NULL并忽略它。device参数可以作为回调函数中访问绑定硬件的传参。

返回值:

  • JDR_OK - 函数执行成功,且编码对象是有效的。
  • JDR_INP - 一个错误发生在input函数,由于硬件错误或者流终止。
  • JDR_MEM1 - 工作空间不足解码这个JPEG图像。
  • JDR_MEM2 - 工作空间不足解码这个JPEG图像。可能更小。
  • JDR_PAR - 参数错误。传入工作空间的指针为NULL。
  • JDR_FMT1 - 数据格式错误。JPEG数据损坏。
  • JDR_FMT2 - 格式正确,但不支持。也许是一个灰度图像。
  • JDR_FMT3 - 不支持JPEG标准,也许是一个后续版本的JPEG图像。

jd_decomp() - 执行解码JPEG图像

jd_decomp()函数解码JPEG图像并输出RGB数据。

jd_decomp()是JPEG解码过程的第二阶段。它进一步执行解码JPEG图像的过程,并通过用户定义的输出函数输出数据,但同时也继续使用在jd_prepare()传入的输入数据流的函数。在它之后,解码对象将不在有效。

在解码时指定的比例因子,它将JPEG图像按1/2、1/4或1/8比例缩放尺寸。例如,当解码一个1024x768大小JPEG图像在1/4比例,它将输出256x192大小。相比不缩放,1/2和1/4的缩放由于求均值,解码速度略有下降。但是1/8缩放相比不缩放是2-3倍的速度输出,因为每个块IDCT和求均值可以跳过,这一特点适合创建缩略图。

JRESULT jd_decomp (
			JDEC* jdec,             /* Pointer to valid decompressor object */
			UINT(*outfunc)(JDEC*,void*,JRECT*), /* Pointer to output function */
			BYTE scale              /* Scaling factor */
			);

输入参数:

  • jdec - 输入,指定有效的解码对象。其实就是之前在jd_prepare()函数中准备好的JDEC对象。
  • outfunc - 输入,指定用户定义的JPEG解码过程输出数据流的回调函数。jd_decomp()调用这个函数去输出解码JPEG图像的RGB形式数据流。具体写法可以看原作者给的应用说明,也可以参见本文中的基于具体微控制器平台的实现。
  • scale - 输入,指定输出缩放值N。输出图像的缩小比例为1/2^N(N = 0 to 3)。当不使用缩放功能时(JD_USE_SCALE == 0),可以指定为0。

返回值

  • JDR_OK - 函数执行成功。
  • JDR_INTR - 解码过程在输出函数中断。
  • JDR_INP - 一个错误发生在input函数,由于硬件错误或者流终止。
  • JDR_PAR - 参数错误。给定的缩放值无效。
  • JDR_FMT1 - 数据格式错误。JPEG数据损坏。

tjpgdcnf.h - 配置文件

早期版本的TjpgDec源码中并没有tjpgdcnf.h文件。我试着找了一下tjpgdec项目的changelog,没有直接找到关于版本更新的详情内容,但是遍历了到目前为止tjpgd所有发布版本的软件包,确定了tjpgdcnf.h首次出现的版本是2021年5月8日发布的R0.02版本。在这个文件中,更细化地提取了一些对TJpgDec软件配置和裁剪的一些选项,在当前最新的R0.03版本中源码如下(原作者非常贴心地注释了可用的选项和对应的解释):

/*----------------------------------------------*/
/* TJpgDec System Configurations R0.03          */
/*----------------------------------------------*/

#define	JD_SZBUF		512
/* 指定在输入数据流中每次读取的字节数,512、1024、2048等等均可。 */

#define JD_FORMAT		0
/* Specifies output pixel format.
/  0: RGB888 (24-bit/pix)
/  1: RGB565 (16-bit/pix)
/  2: Grayscale (8-bit/pix)
*/

#define	JD_USE_SCALE	1
/* Switches output descaling feature.
/  0: Disable
/  1: Enable
*/

#define JD_TBLCLIP		1
/* Use table conversion for saturation arithmetic. A bit faster, but increases 1 KB of code size.
/  0: Disable
/  1: Enable
*/

#define JD_FASTDECODE	0
/* Optimization level
/  0: Basic optimization. Suitable for 8/16-bit MCUs.
/  1: + 32-bit barrel shifter. Suitable for 32-bit MCUs.
/  2: + Table conversion for huffman decoding (wants 6 << HUFF_BIT bytes of RAM)
*/

针对使用ARM Cortex-MC1处理器内核和FSMC驱动的16位并口屏幕的MM32F5微控制器,这里需要改写:

#define JD_FORMAT     1 /* 1: RGB565 (16-bit/pix) */
#define JD_FASTDECODE 1 /* 1: + 32-bit barrel shifter. Suitable for 32-bit MCUs. */

注意,这里似乎还藏了一个彩蛋。同时支持RGB888、RGB565和灰度图像,这意味着TJpgDec软件内部的源码内部包含了原始RGB888转RGB565和灰度图像的算式,这个在纯输出的GUI应用开发中讲会用到,届时可以直接复制验证过的代码片段,而不用我们再自行编写调试啦。

关于TJpgDec的软件许可证

这是一份包含在源代码TJpgDec中的许可声明。

/*----------------------------------------------------------------------------/
/ TJpgDec - Tiny JPEG Decompressor R0.xx                       (C)ChaN, 20xx
/-----------------------------------------------------------------------------/
/ The TJpgDec is a generic JPEG decompressor module for tiny embedded systems.
/ This is a free software that opened for education, research and commercial
/  developments under license policy of following terms.
/
/  Copyright (C) 20xx, ChaN, all right reserved.
/
/ * The TJpgDec module is a free software and there is NO WARRANTY.
/ * No restriction on use. You can use, modify and redistribute it for
/   personal, non-profit or commercial products UNDER YOUR RESPONSIBILITY.
/ * Redistributions of source code must retain the above copyright notice.
/
/----------------------------------------------------------------------------*/

TJpgDec许可是BSD风格的,但存在一些差异。因为TJpgDec是嵌入式项目,对以二进制形式的分发,如嵌入式代码,hex文件或二进制库,未指定以增加其可用性。分发的文档不强制包含关于tjpgdec及其授权文件。TJpgDec是基于GNU GPL兼容的项目。 当有任何修改下重新分发,许可证也可以改为GNU GPL或BSD许可证。

应用接口解析

TJpgDec组件的移植过程仅需要实现两个函数,输入数据和输出数据。但从严格意义上讲,这两个函数都只是以约定的方式将数据流传入到TJpgDec组件,或者从TJpgDec组件中传出到指定存储空间,因为完全是内存到内存的操作,不涉及到任何与具体硬件平台相关绑定关系,所以也算不上移植。甚至在原作者的用户使用文档中,也是以应用笔记(TJpgDec Application Note)作为文档的名字,而不是移植指南。

原作者在《TJpgDec Module Application Note》中讲述了在应用中使用TJpgDec组件的操作步骤。建议用户先试着构建和运行原作者提供的示例程序。

解码会话分为两个阶段。第一阶段是分析JPEG图像,第二阶段是解码。

  1. 初始化输入流。(例如打开一个文件)
  2. 分配JPEG解码对象和工作区域。
  3. 调用jd_prepare()指定输入数据流,并执行分析和准备压缩的JPEG图像。
  4. 使用解码对象中的图像信息初始化输出设备。
  5. 调用jd_decomp()指定输出数据流,解码JPEG图像并输出。

使用轻量级图像解码器TJpgDec - 基于MM32F5微控制器和MindSDK_第3张图片

图x 在应用中使用TJpgDec组件的调用关系图

这是原作者提供的一段参考源码,描述了如何使用TJpgDec模块。

/*------------------------------------------------*/
/* TJpgDec Quick Evaluation Program for PCs       */
/*------------------------------------------------*/
#include 
#include 
#include "tjpgd.h"

/* 用户定义设备标识 */
typedef struct {
    FILE *fp;      /* 用于输入函数的文件指针 */
    BYTE *fbuf;    /* 用于输出函数的帧缓冲区的指针 */
    UINT wfbuf;    /* 帧缓冲区的图像宽度[像素] */
} IODEV;

/*------------------------------*/
/*      用户定义input funciton  */
/*------------------------------*/
UINT in_func (JDEC* jd, BYTE* buff, UINT nbyte)
{
    IODEV *dev = (IODEV*)jd->device; /* Device identifier for the session (5th argument of jd_prepare function) */

    if (buff) {
        /* 从输入流读取一字节 */
        return (UINT)fread(buff, 1, nbyte, dev->fp);
    } else {
        /* 从输入流移除一字节 */
        return fseek(dev->fp, nbyte, SEEK_CUR) ? 0 : nbyte;
    }
}

/*------------------------------*/
/*      用户定义output funciton */
/*------------------------------*/
UINT out_func (JDEC* jd, void* bitmap, JRECT* rect)
{
    IODEV *dev = (IODEV*)jd->device;
    BYTE *src, *dst;
    UINT y, bws, bwd;

    /* 输出进度 */
    if (rect->left == 0) {
        printf("\r%lu%%", (rect->top << jd->scale) * 100UL / jd->height);
    }

    /* 拷贝解码的RGB矩形范围到帧缓冲区(假设RGB888配置) */
    src = (BYTE*)bitmap;
    dst = dev->fbuf + 3 * (rect->top * dev->wfbuf + rect->left);  /* 目标矩形的左上 */
    bws = 3 * (rect->right - rect->left + 1);     /* 源矩形的宽度[字节] */
    bwd = 3 * dev->wfbuf;                         /* 帧缓冲区宽度[字节] */
    for (y = rect->top; y <= rect->bottom; y++) {
        memcpy(dst, src, bws);   /* 拷贝一行 */
        src += bws; dst += bwd;  /* 定位下一行 */
    }

    return 1;    /* 继续解码 */
}

/*------------------------------*/
/*        主程序                */
/*------------------------------*/
int main (int argc, char* argv[])
{
    void *work;       /* 指向解码工作区域 */
    JDEC jdec;        /* 解码对象 */
    JRESULT res;      /* TJpgDec API的返回值 */
    IODEV devid;      /* 用户定义设备标识 */

    /* 打开一个JPEG文件 */
    if (argc < 2) return -1;
    devid.fp = fopen(argv[1], "rb");
    if (!devid.fp) return -1;

    /* 分配一个用于TJpgDec的工作区域 */
    work = malloc(3500);

    /* 准备解码 */
    res = jd_prepare(&jdec, in_func, work, 3500, &devid);
    if (res == JDR_OK) {
        /* 准备好解码。图像信息有效 */
        printf("Image dimensions: %u by %u. %u bytes used.\n", jdec.width, jdec.height, 3100 - jdec.sz_pool);

        devid.fbuf = malloc(3 * jdec.width * jdec.height); /* 输出图像的帧缓冲区(假设RGB888配置) */
        devid.wfbuf = jdec.width;

        res = jd_decomp(&jdec, out_func, 0);   /* 开始1/1缩放解码 */
        if (res == JDR_OK) {
            /* 解码成功。你在这里已经解码图像到帧缓冲区 */
            printf("\rOK  \n");
        } else {
            printf("Failed to decompress: rc=%d\n", res);
        }
        free(devid.fbuf);    /* 释放帧缓冲区 */
    } else {
        printf("Failed to prepare: rc=%d\n", res);
    }

    free(work);             /* 释放工作区域 */
    fclose(devid.fp);       /* 关闭JPEG文件 */

    return res;
}

这里详细说明两个回调函数的写法。

in_func() - 输入数据流回调函数

用户需要在输入数据流的回调函数in_func()中读取JPEG数据存入传参指针buff中。在jd_prepare()函数中传入数据流到TJpgDec模块。

UINT in_func (
			JDEC* jdec,    /* Pointer to the decompression object */
			BYTE* buff,    /* Pointer to buffer to store the read data */
			UINT ndata     /* Number of bytes to read */
			);

输入参数:

  • jdec - 输入,当前服务的jdec对象的handler。通过这个handler可以访问到当前服务的jdec对象的所有资源。
  • buff - 输出,一块内存区,从介质中读取指定数量的字节数据后,存放到这块内存区,交给调用者。但如果buff的值为NULL,就表示跳过ndata参数指定数量的数据。
  • ndata - 输入,调用者希望从输入数据流中读到的字节数量,即buff的大小。或者当buff的值为NULL时,此处参数指定为需要在输入数据流中跳过读取的字节数量。

实际上,这里还有一个隐形的参数,即出现在tjpgdcnf.h文件中的JD_SZBUF,它约定的应该是每次从输入流读取的最大字节数量,即ndata的最大值。

返回值:

  • 返回实际读取或移除的字节数。若返回0,jd_prepare()jd_decomp()函数将终止并返回JDR_INP

out_func() - 输出数据流回调函数

用户可指定解码出来的像素输出到具体的存储区,这个存储区可以是一块内存,或者映射到显存的地址空间。用户需要在out_func()函数内部,在rect参数执行的矩形区域中填充bitmap参数指定的像素信息。

UINT out_func (
			JDEC* jdec,    /* Pointer to the decompression object */
			void* bitmap,  /* RGB bitmap to be output */
			JRECT* rect    /* Rectangular region to output */
			);

输入参数:

  • jdec - 输入,当前服务的jdec对象的handler。通过这个handler可以访问到当前服务的jdec对象的所有资源。
  • bitmap - 像素数据流。是按照tjpgdcnf.h文件中JD_FORMAT选项指定的格式组织,可以是3字节表示的一个像素的RGB888格式,也可以是2字节表示一个像素的RGB565格式等。第一个像素是rect指定矩形区域的左上角,从左到右,从上到下,最后一个像素是右下角的位置。
  • rect - 输入,执行显示像素区域的矩形。JRECT类型的结构体中,有LeftRightTopBottom四个字段,指示当前解码输出的矩形区域。实际上,这个矩形的大小从1x1到16x16不等,取决于图像的裁剪、缩放和采样因子(JPEG信息)。

返回值:

  • 通常返回1,以便TJpgDec继续解码过程。当它返回0,jd_decomp函数终止并返回JDR_INTR

输入数据流可能时常发生变化,因为要读取不同的jpg图像文件。输出数据流也是在应用中根据需要变化的,只不过因为在大多数微控制器应用中,显示设备通常只有一个,所以统一输出到这个显示设备对应的显存中。有一些对于输出体验有要求的场景,需要输出到特定的存储空间,为了进行二次渲染,或者多缓冲区的应用,此时就需要在应用根据程序执行的情况动态切换输出了区域了。

关于工作区和帧缓冲区

在应用程序中调用jd_prepare()函数时,需要为TJpgDec指定一块工作区,作为TJpgDec在内部运行解码算法的临时空间。TJpgDec至少需要3100字节用于JPEG图像,这取决于解码JPEG图像使用怎样的参数。3100字节是在默认输入缓存(JD_SZBUF == 512)下的最大内存需求,并随JD_SZBUFJD_FASTDECODE的配置值变化。JD_SZBUF定义每次从输入流中读取多少字节。TJpgDec对齐每个读请求缓冲区大小,512, 1024, 2048… 字节是从存储设备读取的理想大小。

在样例代码中,原作者使用了动态分配的内存作为工作区,这对于拥有海量存储资源和完善内存管理机制的PC环境是合适的。但在资源受限的嵌入式系统平台上,使用静态内存会是更稳妥的选择。

另外,样例代码中使用了帧缓冲区(devid结构体变量中的fbuf指定内存区,wfbuf指定宽度),在应用程序和out_func回调函数之间传递像素数据,但实际看起来有点莫名其妙。此处了解到以矩形方式传送像素矩阵的模式之后,用户也可以自行简化代码。

在MM32F5微控制器上应用

在MM32F5微控制器上适配TJpgDec时,我使用了FatFs文件系统作为JPEG图像文件的来源,使用静态内存作为工作区,简化了对“帧缓冲区”的使用,并使用LCD模块作为输出设备。最终实现在微控制器系统中启用JPEG图像解码器的功能。

在包含TJpgDec组件的plus-f5270_image_fatfs_tjpgdec_basic_mdk工程中,我将打开图像文件并解码的过程封装成bool app_fs_display_jpg_file(char * filepath)函数,如此,在主循环中遍历到文件系统中的jpg文件后,可以直接使用其文件路径打开文件并显示像素信息到LCD屏上。这个函数中,就使用到了静态内存分配的工具区域app_tjpgdec_work_buff。从代码中可以看出,代码的内容被简化了不少。

#define APP_TJPGDEC_WORK_BUFF_SIZE 3500
uint8_t app_tjpgdec_work_buff[APP_TJPGDEC_WORK_BUFF_SIZE];

/* display a jpg file with its full filepath. */
bool app_fs_display_jpg_file(char * filepath)
{
    JRESULT res;      /* Result code of TJpgDec API */
    JDEC jdec;        /* Decompression object */
    //void *work;       /* Pointer to the work area */
    //size_t sz_work = 3500; /* Size of work area */
    IODEV devid;      /* Session identifier */
    FRESULT fres;
    
    /* Initialize input stream */
    devid.fp = &app_fs_file;
    fres = f_open(devid.fp, filepath, FA_READ);
    if (fres != FR_OK)
    {
        return -1;
    }
    
    /* Prepare to decompress */
    //work = (void*)malloc(sz_work);
    //res = jd_prepare(&jdec, in_func, work, sz_work, &devid);
    res = jd_prepare(&jdec, in_func, app_tjpgdec_work_buff, APP_TJPGDEC_WORK_BUFF_SIZE, &devid);
    if (res == JDR_OK)
    {
        /* It is ready to dcompress and image info is available here */
        //printf("Image size is %u x %u.\n%u bytes of work ares is used.\n", jdec.width, jdec.height, sz_work - jdec.sz_pool);

        /* Initialize output device */
        //devid.fbuf = (uint8_t*)malloc(N_BPP * jdec.width * jdec.height); /* Create frame buffer for output image */
        //devid.wfbuf = jdec.width;

        res = jd_decomp(&jdec, out_func, 0);   /* Start to decompress with 1/1 scaling */
        if (res == JDR_OK)
        {
            /* Decompression succeeded. You have the decompressed image in the frame buffer here. */
            printf("\rDecompression succeeded.\n");
        } else {
            printf("jd_decomp() failed (rc=%d)\n", res);
        }
        //free(devid.fbuf);    /* Discard frame buffer */
    }
    else
    {
        printf("jd_prepare() failed (rc=%d)\n", res);
    }
    //free(work);             /* Discard work area */
    f_close(devid.fp);       /* Close the JPEG file */
    return true;
}

同时,在实现输入输出数据流的回调函数时,也有一些考究。

在实现输入数据流的回调函数的过程中,由于使用了FatFs文件系统,其中很多类POSIX的接口同样例代码的行为并不完全一致,需要做一些转接的工作。特别FatFs文件系统中的f_lseek()函数是从文件开始计算偏移,而不是像通用的f_seek()函数从当前位置算偏移,因此需要使用f_tell()函数做一个适配。

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 */
)
{
    IODEV *dev = (IODEV*)jd->device;   /* Session identifier (5th argument of jd_prepare function) */
    UINT br;

    if (buff) /* Read data from imput stream */
    {
        //return fread(dev->fp, buff, 1, nbyte, dev->fp);
        return (FR_OK == f_read(dev->fp, buff, nbyte, &br)) ? br : 0;
    }
    else /* Remove data from input stream */
    {
        //return f_seek(dev->fp, nbyte, SEEK_CUR) ? 0 : nbyte;
        return (FR_OK == f_lseek(dev->fp, f_tell(dev->fp) + nbyte)) ? nbyte : 0;
    }
}

在实现输出数据流的回调函数的过程中,可以直接对接到开发板上LCD屏的显存中,直接显示像素。

int out_func (      /* Returns 1 to continue, 0 to abort */
    JDEC* jd,       /* Decompression object */
    void* bitmap,   /* Bitmap data to be output */
    JRECT* rect     /* Rectangular region of output image */
)
{
    /* Progress indicator */
    if (rect->left == 0)
    {
        printf("\r%lu%%", (rect->top << jd->scale) * 100UL / jd->height);
    }
    
    /* 在LCD屏幕上显示图像信息. */
    LCD_FillWindow(rect->left, rect->top, rect->right, rect->bottom, (uint16_t *)bitmap);

    return 1;    /* Continue to decompress */
}

在Keil工程中编译可执行文件,下载到plus-f5270开发板上,实物演示如图x所示。

图x 在plus-f5270开发板上运行TJpgDec

Code Size (-0):

==============================================================================

    Total RO  Size (Code + RO Data)                45096 (  44.04kB)
    Total RW  Size (RW Data + ZI Data)             13032 (  12.73kB)
    Total ROM Size (Code + RO Data + RW Data)      45104 (  44.05kB)

==============================================================================

其中,SRAM占用量是比较少的,总共13032字节,其中系统栈占用4KB,系统堆占用4KB,工作空间占用3500字节,还带了个FatFs文件系统,这已经算是非常经济的用量了。

独立的源码工程可见:https://download.csdn.net/download/suyong_yq/87351352

一点思考

从实际演示效果来看,移植的样例工程能够完成JPEG解码功能,验证了TJpgDec组件能够正常工作,这是非常不错的。但由于从SD卡读数、解码、刷屏这些个步骤都是一小段一小段执行的,因此实际显示图像刷屏的速度不是很快(估计受读取SD卡过程的影响较大)。一种改进的策略,是将解码得到的图像片段搜集到一块大内存中,然后集中刷屏,这样视觉效果会好很多。解码完之后纯刷屏的速度很快,解码之前虽然有等待,但当前屏幕上还在放映之前解码的图片,参观者不会感到无趣。

plus-f5270开发板上的MM32F5270微控制器仅有128KB的SRAM,不够存放一整张图(480x320),但是可以存放1/4个屏幕的图片数据(75KB),可以考虑将屏幕分成四块小屏来用,每次刷一块。当然,这将会是下一个故事了。

参考文献

  • 《TJpgDec—轻量级JPEG解码器》,https://www.cnblogs.com/oloroso/p/5851505.html,2022-12-22.
  • 《TJpgDec使用说明》,https://www.cnblogs.com/oloroso/p/5852235.html,2022-12-22.
  • TJpgDec项目主页:http://www.elm-chan.org/fsw/tjpgd/00index.html
  • TJpgDec包含移植方法和应用案例的用户手册:http://www.elm-chan.org/fsw/tjpgd/en/appnote.html
  • TJpgDec到目前为止所有的发布版本软件包(包含最新版):http://www.elm-chan.org/fsw/tjpgd/archives.html
  • ChaN实现了一些非常有趣的应用案例:http://www.elm-chan.org/fsw/tjpgd/tjdemo.mp4

你可能感兴趣的:(#,MM32,嵌入式系统架构设计,算法,java,人工智能)