1)实验平台:正点原子STM32mini开发板
第三十六章图片显示实验
在开发产品的时候,很多时候,我们都会用到图片解码,在本章中,我们将向大家介绍
如何通过STM32来解码BMP/JPG/JPEG/GIF等图片,并在LCD上显示出来。本章分为如
下几个部分:
36.1图片格式简介
36.2硬件设计
36.3软件设计
36.4下载验证
36.1图片格式简介
我们常用的图片格式有很多,一般最常用的有三种:JPEG(或JPG)、BMP和GIF。其中
JPEG(或JPG)和BMP是静态图片,而GIF则是可以实现动态图片。下面,我们简单介绍一
下这三种图片格式。
首先,我们来看看BMP图片格式。BMP(全称Bitmap)是Window操作系统中的标准图
像文件格式,文件后缀名为“.bmp”,使用非常广。它采用位映射存储格式,除了图像深度可
选以外,不采用其他任何压缩,因此,BMP文件所占用的空间很大,但是没有失真。BMP文
件的图像深度可选lbit、4bit、8bit、16bit、24bit及32bit。BMP文件存储数据时,图像的扫描
方式是按从左到右、从下到上的顺序。
典型的BMP图像文件由四部分组成:
1,位图头文件数据结构,它包含BMP图像文件的类型、显示内容等信息;
2,位图信息数据结构,它包含有BMP图像的宽、高、压缩方法,以及定义颜色等信息
3,调色板,这个部分是可选的,有些位图需要调色板,有些位图,比如真彩色图(24位
的BMP)就不需要调色板;
4,位图数据,这部分的内容根据BMP位图使用的位数不同而不同,在24位图中直接使
用RGB,而其他的小于24位的使用调色板中颜色索引值。
关于BMP的详细介绍,请参考光盘的《BMP图片文件详解.pdf》。接下来我们看看JPEG
文件格式。
JPEG是JointPhotographicExpertsGroup(联合图像专家组)的缩写,文件后辍名为“.jpg”
或“.jpeg”,是最常用的图像文件格式,由一个软件开发联合会组织制定,同BMP格式不同,
JPEG是一种有损压缩格式,能够将图像压缩在很小的储存空间,图像中重复或不重要的资料
会被丢失,因此容易造成图像数据的损伤(BMP不会,但是BMP占用空间大)。尤其是使用过
高的压缩比例,将使最终解压缩后恢复的图像质量明显降低,如果追求高品质图像,不宜采用
过高压缩比例。但是JPEG压缩技术十分先进,它用有损压缩方式去除冗余的图像数据,在获
得极高的压缩率的同时能展现十分丰富生动的图像,换句话说,就是可以用最少的磁盘空间得
到较好的图像品质。而且JPEG是一种很灵活的格式,具有调节图像质量的功能,允许用不同
的压缩比例对文件进行压缩,支持多种压缩级别,压缩比率通常在10:1到40:1之间,压缩
比越大,品质就越低;相反地,压缩比越小,品质就越好。比如可以把1.37Mb的BMP位图
文件压缩至20.3KB。当然也可以在图像质量和文件尺寸之间找到平衡点。JPEG格式压缩的
主要是高频信息,对色彩的信息保留较好,适合应用于互联网,可减少图像的传输时间,可以
支持24bit真彩色,也普遍应用于需要连续色调的图像。
JPEG/JPG的解码过程可以简单的概述为如下几个部分:
1、从文件头读出文件的相关信息。
JPEG文件数据分为文件头和图像数据两大部分,其中文件头记录了图像的版本、长宽、
采样因子、量化表、哈夫曼表等重要信息。所以解码前必须将文件头信息读出,以备
图像数据解码过程之用。
2、从图像数据流读取一个最小编码单元(MCU),并提取出里边的各个颜色分量单元。
3、将颜色分量单元从数据流恢复成矩阵数据。
使用文件头给出的哈夫曼表,对分割出来的颜色分量单元进行解码,把其恢复成8×8
的数据矩阵。
4、8×8的数据矩阵进一步解码。
此部分解码工作以8×8的数据矩阵为单位,
其中包括相邻矩阵的直流系数差分解码、使用文件头给出的量化表反量化数据、反Zig-zag编码、隔行正负纠正、反向离散余弦变
换等5个步骤,最终输出仍然是一个8×8的数据矩阵。
5、颜色系统YCrCb向RGB转换。
将一个MCU的各个颜色分量单元解码结果整合起来,将图像颜色系统从YCrCb向
RGB转换。
6、排列整合各个MCU的解码数据。
不断读取数据流中的MCU并对其解码,直至读完所有MCU为止,将各MCU解码
后的数据正确排列成完整的图像。
JPEG的解码本身是比较复杂的,这里FATFS的作者,提供了一个轻量级的JPG/JPEG解
码库:TjpgDec,最少仅需3KB的RAM和3.5KB的FLASH即可实现JPG/JPEG解码,本例程
采用TjpgDec作为JPG/JPEG的解码库,关于TjpgDec的详细使用,请参考光盘:6,软件资料
BMP和JPEG这两种图片格式均不支持动态效果,而GIF则是可以支持动态效果。最后,
我们来看看GIF图片格式。
GIF(GraphicsInterchangeFormat)是CompuServe公司开发的图像文件存储格式,1987年开
发的GIF文件格式版本号是GIF87a,1989年进行了扩充,扩充后的版本号定义为GIF89a。
GIF图像文件以数据块(block)为单位来存储图像的相关信息。一个GIF文件由表示图形/图
像的数据块、数据子块以及显示图形/图像的控制信息块组成,称为GIF数据流(DataStream)。
数据流中的所有控制信息块和数据块都必须在文件头(Header)和文件结束块(Trailer)之间。
GIF文件格式采用了LZW(Lempel-ZivWalch)压缩算法来存储图像数据,定义了允许用户为
图像设置背景的透明(transparency)属性。此外,GIF文件格式可在一个文件中存放多幅彩色图
形/图像。如果在GIF文件中存放有多幅图,它们可以像演幻灯片那样显示或者像动画那样演示。
一个GIF文件的结构可分为文件头(FileHeader)、GIF数据流(GIFDataStream)和文件终结
器(Trailer)三个部分。文件头包含GIF文件署名(Signature)和版本号(Version);GIF数据流由控
制标识符、图象块(ImageBlock)和其他的一些扩展块组成;文件终结器只有一个值为0x3B的字
符(';')表示文件结束。
关于GIF的详细介绍,请参考光盘GIF解码相关资料。图片格式简介,我们就介绍到这里。
36.2硬件设计
本章实验功能简介:开机的时候先检测字库,然后检测SD卡是否存在,如果SD卡存在,
则开始查找SD卡根目录下的PICTURE文件夹,如果找到则显示该文件夹下面的图片文件(支
持bmp、jpg、jpeg或gif格式),循环显示,通过按KEY0和KEY1可以快速浏览下一张和上
一张,WK_UP按键用于暂停/继续播放,DS1用于指示当前是否处于暂停状态。如果未找到
PICTURE文件夹/任何图片文件,则提示错误。同样我们也是用DS0来指示程序正在运行。
所要用到的硬件资源如下:
1)指示灯DS0和DS1
2)KEY0、KEY1和WK_UP三个按键
3)串口
4)TFTLCD模块
5)SD卡
6)SPIFLASH
这几部分,在之前的实例中都介绍过了,我们在此就不介绍了。需要注意的是,我们在SD
卡根目录下要建一个PICTURE的文件夹,用来存放JPEG、JPG、BMP或GIF等图片。36.3软件设计
打开上一章的工程,首先在HARDWARE文件夹所在的文件夹下新建一个PICTURE的文
件夹。在该文件夹里面新建、、、、、、、和
等9个文件。并将PICTURE文件夹加入头文件包含路径。
其中和用于实现对bmp文件的解码;和用于实现对jpeg/jpg文
件的解码;和用于实现对gif文件的解码;这几个代码太长了,所以我们在这里不贴
出来,请大家参考光盘本例程的源码,我们打开,在里面输入如下代码:
include""
_pic_infopicinfo;
//图片信息
_pic_phypic_phy;
//图片显示物理接口
//LCD驱动部分,没有提供划横线函数,需要自己实现
voidpiclib_draw_hline(u16x0,u16y0,u16len,u16color)
{
if((len==0)||()||())return;
LCD_Fill(x0,y0,x0+len-1,y0,color);
}
//填充颜色
//x,y:起始坐标
//width,height:宽度和高度。
//*color:颜色数组
voidpiclib_fill_color(u16x,u16y,u16width,u16height,u16*color)
{
LCD_Color_Fill(x,y,x+width-1,y+height-1,color);
}
//画图初始化,在画图之前,必须先调用此函数
//指定画点/读点
voidpiclib_init(void)
{
pic__point=LCD_ReadPoint;
//读点函数实现
pic__point=LCD_Fast_DrawPoint;//画点函数实现
pic_=LCD_Fill;
//填充函数实现
pic__hline=piclib_draw_hline;//画线函数实现
pic_=piclib_fill_color;
//颜色填充函数实现
=;
//得到LCD的宽度像素
=;//得到LCD的高度像素
=0;
//初始化宽度为0
=0;//初始化高度为0
_Fac=0;
//初始化缩放系数为0
_Height=0;
//初始化设定的高度为0
_Width=0;
//初始化设定的宽度为0
_XOFF=0;
//初始化x轴的偏移量为0
_YOFF=0;
//初始化y轴的偏移量为0
=0;
//初始化当前显示到的x坐标为0
=0;
//初始化当前显示到的y坐标为0
}
//快速ALPHABLENDING算法.
//src:源颜色
//dst:目标颜色
//alpha:透明程度(0~32)
//返回值:混合后的颜色.
u16piclib_alpha_bl(u16src,u16dst,u8alpha)
{
u32src2;u32dst2;
//Convertto32bit|-----GGGGGG-----RRRRR------BBBBB|
src2=((src16)|src)0x07E0F81F;
dst2=((dst16)|dst)0x07E0F81F;
//PerformblingR:G:Bwithalphainrange0..32
//Notethatthereasonthatalphamaynotexceed32isthatthereareonly
//5bitsofspacebetweeneachR:G:Bvalue,anyhighervaluewilloverflow
//intothenextcomponentanddeliveruglyresult.
dst2=((((dst2-src2)*alpha)5)+src2)0x07E0F81F;
return(dst216)|dst2;
}
//初始化智能画点
//内部调用
voidai_draw_init(void)
{
floattemp,temp1;
temp=(float)_Width/;
temp1=(float)_Height/;
if(temptemp1)temp1=temp;//取较小的那个
if(temp11)temp1=1;
//使图片处于所给区域的中间
_XOFF+=(_Width-temp1*)/2;
_YOFF+=(_Height-temp1*)/2;
temp1*=8192;//扩大8192倍
_Fac=temp1;
=0xffff;
=0xffff;//放到一个不可能的值上面
}
//判断这个像素是否可以显示
//(x,y):像素原始坐标
//chg:功能变量.
//返回值:0,不需要显示.1,需要显示
u8is_element_ok(u16x,u16y,u8chg)
{
if(x!=||y!=)
{
if(chg==1)
{
=x;
=y;
}
return1;
}elsereturn0;
}
//智能画图
//FileName:要显示的图片文件BMP/JPG/JPEG/GIF
//x,y,width,height:坐标及显示区域尺寸
//fast:使能jpeg/jpg小图片(图片尺寸小于等于液晶分辨率)快速解码,0,不使能;1,使能.
//图片在开始和结束的坐标点范围内显示
u8ai_load_picfile(constu8*filename,u16x,u16y,u16width,u16height,u8fast)
{
u8res;//返回值
u8temp;
if((x+width))returnPIC_WINDOW_ERR;
//x坐标超范围了.
if((y+height))returnPIC_WINDOW_ERR;
//y坐标超范围了.
//得到显示方框大小
if(width==0||height==0)returnPIC_WINDOW_ERR;//窗口设定错误
_Height=height;
_Width=width;
//显示区域无效
if(_Height==0||_Width==0)
{
_Height=;
_Width=;
returnFALSE;
}
if(pic_==NULL)fast=0;//颜色填充函数未实现,不能快速显示
//显示的开始坐标点
_YOFF=y;
_XOFF=x;
//文件名传递
temp=f_typetell((u8*)filename);//得到文件的类型
switch(temp)
{
caseT_BMP:
res=stdbmp_decode(filename);
//解码bmp
break;
caseT_JPG:
caseT_JPEG:
res=jpg_decode(filename,fast);
//解码JPG/JPEG
break;
caseT_GIF:
res=gif_decode(filename,x,y,width,height);//解码gif
break;
default:
res=PIC_FORMAT_ERR;
//非图片格式!!!
break;
}
returnres;
}
//动态分配内存
void*pic_memalloc(u32size)
{
return(void*)mymalloc(size);
}
//释放内存
voidpic_memfree(void*mf)
{
myfree(mf);
}
此段代码总共9个函数,其中,piclib_draw_hline和piclib_fill_color函数因为LCD驱动代
码没有提供,所以在这里单独实现,如果LCD驱动代码有提供,则直接用LCD提供的即可。
piclib_init函数,该函数用于初始化图片解码的相关信息,其中_pic_phy是我们在
里面定义的一个结构体,用于管理底层LCD接口函数,这些函数必须由用户在外部实现。
_pic_info则是另外一个结构体,用于图片缩放处理。
piclib_alpha_bl函数,该函数用于实现半透明效果,在小格式(分辨率小于240*320)
bmp解码的时候,可能被用到。
ai_draw_init函数,该函数用于实现图片在显示区域的居中显示初始化,其实就是根据图片
大小选择缩放比例和坐标偏移值。
is_element_ok函数,该函数用于判断一个点是不是应该显示出来,在图片缩放的时候该函
数是必须用到的。
ai_load_picfile函数,该函数是整个图片显示的对外接口,外部程序,通过调用该函数,可
以实现bmp、jpg/jpeg和gif的显示,该函数根据输入文件的后缀名,判断文件格式,然后交给
相应的解码程序(bmp解码/jpeg解码/gif解码),执行解码,完成图片显示。注意,这里我们用
到一个f_typetell的函数,来判断文件的后缀名,f_typetell函数在里面实现,具体请参
考光盘源码。
最后,pic_memalloc和pic_memfree分别用于图片解码时需要用到的内存申请和释放,通
过调用mymalloc和myfreee来实现。
保存,然后在工程里面新建一个PICTURE的分组,将、、和
等4个c文件加入到PICTURE分组下。然后打开,在该文件输入如下代码:
define__PICLIB_H
include""
include""
include""
include""
definePIC_SIZE_ERR
0x28
//图片尺寸错误
definePIC_MEM_ERR
0x11
//内存错误
defineTRUE1
ifndefFALSE
if
//图片显示物理层接口
//在移植的时候,必须由用户自己实现这几个函数
typedefstruct
{
u16(*read_point)(u16,u16);
//u16read_point(u16x,u16y)
读点函数
void(*draw_point)(u16,u16,u16);//voiddraw_point(u16x,u16y,u16color)画点函数
void(*fill)(u16,u16,u16,u16,u16);
//voidfill(u16sx,u16sy,u16ex,u16ey,u16color)单色填充函数
void(*draw_hline)(u16,u16,u16,u16);
//voiddraw_hline(u16x0,u16y0,u16len,u16color)画水平线函数
void(*fillcolor)(u16,u16,u16,u16,u16*);
//voidpiclib_fill_color(u16x,u16y,u16width,u16height,u16*color)颜色填充
}_pic_phy;
extern_pic_phypic_phy;
//图像信息
typedefstruct
{
u16lcdwidth;
//LCD的宽度
u16lcdheight;
//LCD的高度
u32ImgWidth;
//图像的实际宽度和高度
u32ImgHeight;
u32Div_Fac;
//缩放系数(扩大了8192倍的)
u32S_Height;
//设定的高度和宽度
u32S_Width;
u32S_XOFF;//x轴和y轴的偏移量
u32S_YOFF;
u32staticx;//当前显示到的xy坐标
u32staticy;
}_pic_info;
extern_pic_infopicinfo;//图像信息
voidpiclib_init(void);
//初始化画图
u16piclib_alpha_bl(u16src,u16dst,u8alpha);
//alphabl处理
voidai_draw_init(void);
//初始化智能画图
u8is_element_ok(u16x,u16y,u8chg);
//判断像素是否有效
u8ai_load_picfile(constu8*filename,u16x,u16y,u16width,u16height,u8fast);//智能画图
void*pic_memalloc(u32size);//pic申请内存
voidpic_memfree(void*mf);
//pic释放内存
#if
这里基本就是我们前面提到的两个结构体的定义以及一些函数的申明,保存。最后
我们在文件里面修改代码如下:
//得到path路径下,目标文件的总个数
//path:路径
//返回值:总有效文件数
u16pic_get_tnum(u8*path)
{
u8res;u16rval=0;
DIRtdir;
//临时目录
FILINFOtfileinfo;//临时文件信息
u8*fn;
res=f_opir(tdir,(constTCHAR*)path);
//打开目录
=_MAX_LFN*2+1;
//长文件名最大长度
=mymalloc();//为长文件缓存区分配内存
if(res==FR_!=NULL)
{
while(1)//查询总的有效文件数
{
res=f_readdir(tdir,tfileinfo);
//读取目录下的一个文件
if(res!=FR_OK||[0]==0)break;
//错误了/到末尾了,退出
fn=(u8*)(*?:);
res=f_typetell(fn);
if((res0XF0)==0X50)//取高四位,看看是不是图片文件
{
rval++;//有效文件数增加1
}
}
}
returnrval;
}
intmain(void)
{
u8res;u8t;u16temp;
DIRpicdir;
//图片目录
FILINFOpicfileinfo;
//文件信息
u8*fn;
//长文件名
u8*pname;
//带路径的文件名
u16totpicnum;
//图片文件总数
u16curindex;
//图片当前索引
u8key;
//键值
u8pause=0;
//暂停标记
u16*picindextbl;
//图片索引表
HAL_Init();
//初始化HAL库
Stm32_Clock_Init(RCC_PLL_MUL9);//设置时钟,72M
delay_init(72);
//初始化延时函数
uart_init(115200);
//初始化串口
usmart_(84);
//初始化USMART
LED_Init();
//初始化LED
KEY_Init();
//初始化按键
LCD_Init();
//初始化LCD
mem_init();
//初始化内存池
exfuns_init();
//为fatfs相关变量申请内存
f_mount(fs[0],"0:",1);
//挂载SD卡
f_mount(fs[1],"1:",1);
//挂载FLASH.
POINT_COLOR=RED;
while(font_init())
//检查字库
{
LCD_ShowString(60,50,200,16,16,"FontError!");delay_ms(200);
LCD_Fill(60,50,240,66,WHITE);delay_ms(200);
}
Show_Str(30,50,200,16,"MiniSTM32开发板",16,0);
Show_Str(30,70,200,16,"图片显示程序",16,0);
Show_Str(30,90,200,16,"KEY0:NEXTKEY1:PREV",16,0);
Show_Str(30,110,200,16,"KEY_UP:PAUSE",16,0);
Show_Str(30,130,200,16,"正点原子@ALIENTEK",16,0);
Show_Str(30,150,200,16,"2019年11月18日",16,0);
while(f_opir(picdir,"0:/PICTURE"))//打开图片文件夹
{
Show_Str(60,170,240,16,"PICTURE文件夹错误!",16,0);delay_ms(200);
LCD_Fill(60,170,240,186,WHITE);delay_ms(200);
}
totpicnum=pic_get_tnum("0:/PICTURE");//得到总有效文件数
while(totpicnum==NULL)//图片文件为0
{
Show_Str(60,170,240,16,"没有图片文件!",16,0);delay_ms(200);
LCD_Fill(60,170,240,186,WHITE);delay_ms(200);//清除显示
}
=_MAX_LFN*2+1;
//长文件名最大长度
=mymalloc();//为长文件缓存区分配内存
pname=mymalloc();
//为带路径的文件名分配内存
picindextbl=mymalloc(2*totpicnum);//申请2*totpicnum个字节内存,用于存放图片索引
while(==NULL||pname==NULL||picindextbl==NULL)//内存分配出错
{
Show_Str(60,170,240,16,"内存分配失败!",16,0);delay_ms(200);
LCD_Fill(60,170,240,186,WHITE);delay_ms(200);
}
//记录索引
res=f_opir(picdir,"0:/PICTURE");//打开目录
if(res==FR_OK)
{
curindex=0;//当前索引为0
while(1)//全部查询一遍
{
temp=;
//记录当前index
res=f_readdir(picdir,picfileinfo);
//读取目录下的一个文件
if(res!=FR_OK||[0]==0)break;//错误了/到末尾了,退出
fn=(u8*)(*?:);
res=f_typetell(fn);
if((res0XF0)==0X50)//取高四位,看看是不是图片文件
{
picindextbl[curindex]=temp;//记录索引
curindex++;
}
}
}
Show_Str(60,170,240,16,"开始显示",16,0);
delay_ms(1500);
piclib_init();
//初始化画图
curindex=0;
//从0开始显示
res=f_opir(picdir,(constTCHAR*)"0:/PICTURE");//打开目录
while(res==FR_OK)//打开成功
{
dir_sdi(picdir,picindextbl[curindex]);//改变当前目录索引
res=f_readdir(picdir,picfileinfo);
//读取目录下的一个文件
if(res!=FR_OK||[0]==0)break;//错误了/到末尾了,退出
fn=(u8*)(*?:);
strcpy((char*)pname,"0:/PICTURE/");
//复制路径(目录)
strcat((char*)pname,(constchar*)fn);
//将文件名接在后面
LCD_Clear(BLACK);
ai_load_picfile(pname,0,0,,,1);//显示图片
Show_Str(2,2,240,16,pname,16,1);
//显示图片名字
t=0;
while(1)
{
key=KEY_Scan(0);
//扫描按键
if(t250)key=1;
//模拟一次按下KEY0
if((t%20)==0)LED0=!LED0;//LED0闪烁,提示程序正在运行.
if(key==KEY1_PRES)
//上一张
{
if(curindex)curindex--;
elsecurindex=totpicnum-1;
break;
}elseif(key==KEY0_PRES)//下一张
{
curindex++;
if(curindex=totpicnum)curindex=0;//到末尾的时候,自动从头开始
break;
}elseif(key==WKUP_PRES)
{
pause=!pause;
LED1=!pause;
//暂停的时候LED1亮.
}
if(pause==0)t++;
delay_ms(10);
}
res=0;
}
myfree();//释放内存
myfree(pname);
//释放内存
myfree(picindextbl);
//释放内存
}
此部分除了mian函数,还有一个pic_get_tnum的函数,用来得到path路径下,所有有效
文件(图片文件)的个数。在mian函数里面我们通过索引(图片文件在PICTURE文件夹下的
编号),来查找上一个/下一个图片文件,这里我们需要用到fatfs自带的一个函数:dir_sdi,来
设置当前目录的索引(因为f_readdir只能沿着索引一直往下找,不能往上找),方便定位到任
何一个文件。dir_sdi在FATFS下面被定义为static函数,所以我们必须在里面将该函数的
static修饰词去掉,然后在里面添加该函数的申明,以便main函数使用。
其他部分就比较简单了,至此,整个图片显示实验的软件设计部分就结束了。该程序将实
现浏览PICTURE文件夹下的所有图片,并显示其名字,每隔3s左右切换一幅图片。
36.4下载验证
在代码编译成功之后,我们下载代码到ALIENTEKMiniSTM32开发板上,可以看到LCD
开始显示图片(假设SD卡及图片文件都已经准备好了),如图36.4.1所示:
图36.4.1图片显示实验显示效果
按KEY0和KEY1可以快速切换到下一张或上一张,WK_UP按键可以暂停自动播放,同
时DS1亮,指示处于暂停状态,再按一次WK_UP则继续播放(DS1灭)。同时,由于我们的
代码支持gif格式的图片显示(注意尺寸不能超过LCD屏幕尺寸),所以可以放一些gif图片到
PICTURE文件夹,来看动画了。
本章,同样可以通过USMART来测试该实验,将ai_load_picfile函数加入USMART控制(方法前面已经讲了很多次了),就可以通过串口调用该函数,在屏幕上任何区域显示任何你想要显示的图片了!
版权声明:本站所有作品(图文、音视频)均由用户自行上传分享,仅供网友学习交流,不声明或保证其内容的正确性,如发现本站有涉嫌抄袭侵权/违法违规的内容。请举报,一经查实,本站将立刻删除。