正点原子stm32全套讲解(F7水星开发板资料连载第四十九章)
正点原子stm32全套讲解(F7水星开发板资料连载第四十九章)49.3 软件设计49.2 硬件设计上一章,我们学习了图片解码,本章我们将学习 BMP&JPEG 编码,结合前面的摄像头实验,实现一个简单的照相机。本章分为如下几个部分:49.1 BMP&JPEG 编码简介
1)实验平台:正点原子水星 STM32F4/F7 开发板
2)摘自《STM32F7 开发指南(HAL 库版)》关注官方微信号公众号,获取更多资料:正点原子
3)全套实验源码 手册 视频下载地址:http://www.openedv.com/thread-13912-1-1.html
第四十九章 照相机实验
上一章,我们学习了图片解码,本章我们将学习 BMP&JPEG 编码,结合前面的摄像头实
验,实现一个简单的照相机。本章分为如下几个部分:
49.1 BMP&JPEG 编码简介
49.2 硬件设计
49.3 软件设计
49.4 下载验证
49.1 BMP&JPEG 编码简介
本章,我们要实现的照相机,支持 BMP 图片格式的照片和 JPEG 图片格式的照片,这里简
单介绍一下这两种图片格式的编码。这里我们使用 ATK-OV5640-AF 摄像头,来实现拍照。关
于 OV5640 的相关知识点,请参考第四十一章。
49.1.1 BMP 编码简介
上一章,我们学习了各种图片格式的解码。本章,我们介绍最简单的图片编码方法:BMP
图片编码。通过前面的了解,我们知道 BMP 文件是由文件头、位图信息头、颜色信息和图形
数据等四部分组成。我们先来了解下这几个部分。
1、BMP 文件头(14 字节):BMP 文件头数据结构含有 BMP 文件的类型、文件大小和位图起
始位置等信息。
//BMP 文件头
typedef __packed struct
{
u16 bfType ;
//文件标志.只对'BM' 用来识别 BMP 位图类型
u32 bfSize ;
//文件大小 占四个字节
u16 bfReserved1 ;
//保留
u16 bfReserved2 ;
//保留
u32 bfOffBits ;
//从文件开始到位图数据(bitmap data)开始之间的偏移量
}BITMAPFILEHEADER ;
2、位图信息头(40 字节):BMP 位图信息头数据用于说明位图的尺寸等信息。
typedef __packed struct
{
u32 biSize ;
//说明 BITMAPINFOHEADER 结构所需要的字数。
long biWidth ;
//说明图象的宽度,以象素为单位
long biHeight ; //说明图象的高度,以象素为单位
u16 biPlanes ;
//为目标设备说明位面数,其值将总是被设为 1
u16 biBitCount ; //说明比特数/象素,其值为 1、4、8、16、24、或 32
u32 biCompression ; //说明图象数据压缩的类型。其值可以是下述值之一:
//BI_RGB:没有压缩;
//BI_RLE8:每个象素 8 比特的 RLE 压缩编码,压缩格式由 2 字节组成
//BI_RLE4:每个象素 4 比特的 RLE 压缩编码,压缩格式由 2 字节组成
//BI_BITFIELDS:每个象素的比特由指定的掩码决定。
u32 biSizeImage ;//说明图象的大小 以字节为单位。当用 BI_RGB 格式时 可设置为 0
long biXPelsPerMeter ;//说明水平分辨率,用象素/米表示
long biYPelsPerMeter ;//说明垂直分辨率,用象素/米表示
u32 biClrUsed ;
//说明位图实际使用的彩色表中的颜色索引数
u32 biClrImportant ; //说明对图象显示有重要影响的颜色索引的数目,
//如果是 0,表示都重要。
}BITMAPINFOHEADER ;
3、颜色表:颜色表用于说明位图中的颜色,它有若干个表项,每一个表项是一个 RGBQUAD
类型的结构,定义一种颜色。
typedef __packed struct
{
u8 rgbBlue ; //指定蓝色强度
u8 rgbGreen ;
//指定绿色强度
u8 rgbRed ; //指定红色强度
u8 rgbReserved ; //保留,设置为 0
}RGBQUAD ;
颜色表中 RGBQUAD 结构数据的个数由 biBitCount 来确定:当 biBitCount=1、4、8 时,分
别有 2、16、256 个表项;当 biBitCount 大于 8 时,没有颜色表项。
BMP 文件头、位图信息头和颜色表组成位图信息(我们将 BMP 文件头也加进来,方便处
理),BITMAPINFO 结构定义如下:
typedef __packed struct
{
BITMAPFILEHEADER bmfHeader;
BITMAPINFOHEADER bmiHeader;
RGBQUAD bmiColors[1];
}BITMAPINFO;
4、位图数据:位图数据记录了位图的每一个像素值,记录顺序是在扫描行内是从左到右,扫描
行之间是从下到上。位图的一个像素值所占的字节数:
当 biBitCount=1 时,8 个像素占 1 个字节;
当 biBitCount=4 时,2 个像素占 1 个字节;
当 biBitCount=8 时,1 个像素占 1 个字节;
当 biBitCount=16 时,1 个像素占 2 个字节;
当 biBitCount=24 时,1 个像素占 3 个字节;
当 biBitCount=32 时,1 个像素占 4 个字节;
biBitCount=1 表示位图最多有两种颜色,缺省情况下是黑色和白色,你也可以自己定义这
两种颜色。图像信息头装调色板中将有两个调色板项,称为索引 0 和索引 1。图象数据阵列中
的每一位表示一个像素。如果一个位是 0,显示时就使用索引 0 的 RGB 值,如果位是 1,则使
用索引 1 的 RGB 值。
biBitCount=16 表示位图最多有 65536 种颜色。每个像素用 16 位(2 个字节)表示。这种
格式叫作高彩色,或叫增强型 16 位色,或 64K 色。它的情况比较复杂,当 biCompression 成员
的值是 BI_RGB 时,它没有调色板。16 位中,最低的 5 位表示蓝色分量,中间的 5 位表示绿色
分量,高的 5 位表示红色分量,一共占用了 15 位,最高的一位保留,设为 0。这种格式也被称作 555 16 位位图。如果 biCompression 成员的值是 BI_BITFIELDS,那么情况就复杂了,首先
是原来调色板的位置被三个 DWORD 变量占据,称为红、绿、蓝掩码。分别用于描述红、绿、
蓝分量在 16 位中所占的位置。在 Windows 95(或 98)中,系统可接受两种格式的位域:555
和 565,在 555 格式下,红、绿、蓝的掩码分别是:0x7C00、0x03E0、0x001F,而在 565 格式
下,它们则分别为:0xF800、0x07E0、0x001F。你在读取一个像素之后,可以分别用掩码“与”
上像素值,从而提取出想要的颜色分量(当然还要再经过适当的左右移操作)。在 NT 系统中,
则没有格式限制,只不过要求掩码之间不能有重叠。(注:这种格式的图像使用起来是比较麻烦
的,不过因为它的显示效果接近于真彩,而图像数据又比真彩图像小的多,所以,它更多的被
用于游戏软件)。
biBitCount=32 表示位图最多有 4294967296(2 的 32 次方)种颜色。这种位图的结构与 16 位
位图结构非常类似,当 biCompression 成员的值是 BI_RGB 时,它也没有调色板,32 位中有 24
位用于存放 RGB 值,顺序是:最高位—保留,红 8 位、绿 8 位、蓝 8 位。这种格式也被成为
888 32 位图。如果 biCompression 成员的值是 BI_BITFIELDS 时,原来调色板的位置将被三个
DWORD 变量占据,成为红、绿、蓝掩码,分别用于描述红、绿、蓝分量在 32 位中所占的位
置。在 Windows 95(or 98)中,系统只接受 888 格式,也就是说三个掩码的值将只能是:0xFF0000、
0xFF00、0xFF。而在 NT 系统中,你只要注意使掩码之间不产生重叠就行。(注:这种图像格
式比较规整,因为它是 DWORD 对齐的,所以在内存中进行图像处理时可进行汇编级的代码优
化(简单))。
通过以上了解,我们对 BMP 有了一个比较深入的了解,本章,我们采用 16 位 BMP 编码
(因为我们的 LCD 就是 16 位色的,而且 16 位 BMP 编码比 24 位 BMP 编码更省空间),故我
们需要设置 biBitCount 的值为 16,这样得到新的位图信息(BITMAPINFO)结构体:
typedef __packed struct
{
BITMAPFILEHEADER bmfHeader;
BITMAPINFOHEADER bmiHeader;
u32 RGB_MASK[3];
//调色板用于存放 RGB 掩码.
}BITMAPINFO;
其实就是颜色表由 3 个 RGB 掩码代替。最后,我们来看看将 LCD 的显存保存为 BMP 格
式的图片文件的步骤:
1)创建 BMP 位图信息,并初始化各个相关信息
这里,我们要设置 BMP 图片的分辨率为 LCD 分辨率、BMP 图片的大小(整个 BMP 文件
大小)、BMP 的像素位数(16 位)和掩码等信息。
2)创建新 BMP 文件,写入 BMP 位图信息
我们要保存 BMP,当然要存放在某个地方(文件),所以需要先创建文件,同时先保存 BMP
位图信息,之后才开始 BMP 数据的写入。
3)保存位图数据。
这里就比较简单了,只需要从 LCD 的 GRAM 里面读取各点的颜色值,依次写入第二步创
建的 BMP 文件即可。注意:保存顺序(即读 GRAM 顺序)是从左到右,从下到上。
4)关闭文件。
使用 FATFS,在文件创建之后,必须调用 f_close,文件才会真正体现在文件系统里面,否
则是不会写入的!这个要特别注意,写完之后,一定要调用 f_close。
BMP 编码就介绍到这里。
49.1.2 JPEG 编码简介
JPEG(Joint Photographic Experts Group)是一个由 ISO 和 IEC 两个组织机构联合组成的一
个专家组,负责制定静态的数字图像数据压缩编码标准,这个专家组开发的算法称为 JPEG 算
法,并且成为国际上通用的标准,因此又称为 JPEG 标准。JPEG 是一个适用范围很广的静态图
像数据压缩标准,既可用于灰度图像又可用于彩色图像。
JPEG 专家组开发了两种基本的压缩算法,一种是采用以离散余弦变换(Discrete Cosine
Transform,DCT)为基础的有损压缩算法,另一种是采用以预测技术为基础的无损压缩算法。
使用有损压缩算法时,在压缩比为 25:1 的情况下,压缩后还原得到的图像与原始图像相比较,
非图像专家难于找出它们之间的区别,因此得到了广泛的应用。
JPEG 压缩是有损压缩,它利用了人的视角系统的特性,使用量化和无损压缩编码相结合
来去掉视角的冗余信息和数据本身的冗余信息。
JPEG 压缩编码分为三个步骤:
1)使用正向离散余弦变换(Forward Discrete Cosine Transform,FDCT)把空间域表示的图
变换成频率域表示的图。
2)使用加权函数对 DCT 系数进行量化,这个加权函数对于人的视觉系统是最佳的。
3)使用霍夫曼可变字长编码器对量化系数进行编码。
这里我们不详细介绍 JPEG 压缩的过程了,大家可以自行查找相关资料。我们本章要实现
的 JPEG 拍照,并不需要自己压缩图像,因为我们使用的 ALIENTEK OV5640 摄像头模块,直
接就可以输出压缩后的 JPEG 数据,我们完全不需要理会压缩过程,所以本章我们实现 JPEG
拍照的关键,在于准确接收 OV5640 摄像头模块发送过来的编码数据,然后将这些数据保存
为.jpg 文件,就可以实现 JPEG 拍照了。
在第四十二章,我们定义了一个很大的数组 jpeg_data_buf(4MB)来存储 JPEG 图像数据,
但本章,我们可以使用内存管理来申请内存,无需定义这么大的数组,使用上更加灵活。另外,
DCMI 接口使用 DMA 直接传输 JPEG 数据到外部 SDRAM 会出现数据丢失,所以 DMA 接收
JPEG 数据只能用内部 SRAM,然后再拷贝到外部 SDRAM。所以,我们本章将使用 DMA 的双
缓冲机制来读取,DMA 双缓冲读取 JPEG 数据框图如图 49.1.2.1 所示:
图 49.1.2.1 DMA 双缓冲读取 JPEG 数据原理框图
DMA 接收来自 OV5640 的 JPEG 数据流,首先使用 M0AR(内存 1)来存储,当 M0AR 满
了以后,自动切换到 M1AR(内存 2),同时程序读取 M0AR(内存 1)的数据到外部 SDRAM;
当 M1AR 满了以后,又切回 M0AR,同时程序读取 M1AR(内存 2)的数据到外部 SDRAM;
依次循环(此时的数据处理,是通过 DMA 传输完成中断实现的,在中断里面处理),直到帧中
断,结束一帧数据的采集,读取剩余数据到外部 SDRAM,完成一次 JPEG 数据的采集。
这里,M0AR,M1AR 所指向的内存,必须是内部内存,不过由于采用了双缓冲机制,我们就不必定义一个很大的数组,一次性接收所有 JPEG 数据了,而是可以分批次接收,数组可
以定义的比较小。
最后,将存储在外部 SDRAM 的 jpeg 数据,保存为.jpg/.jpeg 存放在 SD 卡,就完成了一次
JPEG 拍照。
49.2 硬件设计
本章实验功能简介:开机的时候先检测字库,然后检测 SD 卡根目录是否存在 PHOTO 文
件夹,如果不存在则创建,如果创建失败,则报错(提示拍照功能不可用)。在找到 SD 卡的
PHOTO 文件夹后,开始初始化 OV5640,在初始化成功之后,就一直在屏幕显示 OV5640 拍
到的内容。当按下 KEY_UP 按键的时候,可以选择缩放,还是 1:1 显示,默认缩放。按下 KEY0,
可以拍 bmp 图片照片(分辨率为:LCD 辨率)。按下 KEY1 可以拍 JPEG 图片照片(分辨率
为 QSXGA,即 2592 *1944)。拍照保存成功之后,蜂鸣器会发出“滴”的一声,提示拍照成
功。DS0 还是用于指示程序运行状态,DS1 用于提示 DCMI 帧中断。
所要用到的硬件资源如下:
1) 指示灯 DS0 和 DS1
2) KEY0、KEY1 和 KEY_UP 按键
3) TPAD 按键
4) 串口
5) LCD 模块
6) SD 卡
7) SPI FLASH
8) OV5640 摄像头模块
这几部分,在之前的实例中都介绍过了,我们在此就不介绍了。需要注意的是:SD 卡与
DCMI 接口有部分 IO 共用,所以他们不能同时使用,必须分时复用,本章,这部分共用 IO 我
们只有在拍照保存的时候,才切换为 SD 卡使用,其他时间,都是被 DCMI 占用的。
49.3 软件设计
打开本章实验工程,由于本章要用到 OV5640、蜂鸣器和 SD 卡,所以,先添加了 dcmi.c、
sccb.c、ov5640.c、beep.c 和 sdram.c 等文件到 HARDWARE 组下。
然后,我们来看下 PICTURE 组下的 bmp.c 文件里面的 bmp 编码函数:bmp_encode,该函
数代码如下:
//BMP 编码函数
//将当前 LCD 屏幕的指定区域截图 存为 16 位格式的 BMP 文件 RGB565 格式.
//保存为 rgb565 则需要掩码 需要利用原来的调色板位置增加掩码.这里我们增加了掩码.
//保存为 rgb555 格式则需要颜色转换 耗时间比较久 所以保存为 565 是最快速的办法.
//filename:存放路径
//x y:在屏幕上的起始坐标
//mode:模式.0 仅创建新文件;1 如果存在文件 则覆盖该文件.如果没有 则创建新的文件.
//返回值:0 成功;其他 错误码.
u8 bmp_encode(u8 *filename u16 x u16 y u16 width u16 height u8 mode)
{
FIL* f_bmp; u8 res=0;
u16 bmpheadsize;
//bmp 头大小
BITMAPINFO hbmp;
//bmp 头
u16 tx ty;
//图像尺寸
u16 *databuf;
//数据缓存区地址
u16 pixcnt;
//像素计数器
u16 bi4width;
//水平像素字节数
if(width==0||height==0)return PIC_WINDOW_ERR; //区域错误
if((x width-1)>lcddev.width)return PIC_WINDOW_ERR;
//区域错误
if((y height-1)>lcddev.height)return PIC_WINDOW_ERR;
//区域错误
#if BMP_USE_MALLOC == 1 //使用 malloc
databuf=(u16*)pic_memalloc(1024);
//开辟至少 bi4width 大小的字节的内存区域 对 240 宽的屏 480 个字节就够了.
if(databuf==NULL)return PIC_MEM_ERR;
//内存申请失败.
f_bmp=(FIL *)pic_memalloc(sizeof(FIL));
//开辟 FIL 字节的内存区域
if(f_bmp==NULL){pic_memfree(databuf); return PIC_MEM_ERR; }//内存申请失败.
#else
databuf=(u16*)bmpreadbuf;
f_bmp=&f_bfile;
#endif
bmpheadsize=sizeof(hbmp);
//得到 bmp 文件头的大小
mymemset((u8*)&hbmp 0 sizeof(hbmp));// 申请到的内存置零.
hbmp.bmiHeader.biSize=sizeof(BITMAPINFOHEADER);//信息头大小
hbmp.bmiHeader.biWidth=width;
//bmp 的宽度
hbmp.bmiHeader.biHeight=height;
//bmp 的高度
hbmp.bmiHeader.biPlanes=1;
//恒为 1
hbmp.bmiHeader.biBitCount=16;
//bmp 为 16 位色 bmp
hbmp.bmiHeader.biCompression=BI_BITFIELDS;//每个象素的比特由指定的掩码决定。
hbmp.bmiHeader.biSizeImage=hbmp.bmiHeader.biHeight*hbmp.bmiHeader.biWidth*
hbmp.bmiHeader.biBitCount/8;//bmp 数据区大小
hbmp.bmfHeader.bfType=((u16)'M'<<8) 'B';
//BM 格式标志
hbmp.bmfHeader.bfSize=bmpheadsize hbmp.bmiHeader.biSizeImage;//整个 bmp 的大小
hbmp.bmfHeader.bfOffBits=bmpheadsize;
//到数据区的偏移
hbmp.RGB_MASK[0]=0X00F800;
//红色掩码
hbmp.RGB_MASK[1]=0X0007E0;
//绿色掩码
hbmp.RGB_MASK[2]=0X00001F;
//蓝色掩码
if(mode==1)res=f_open(f_bmp (const TCHAR*)filename FA_READ|FA_WRITE);
//尝试打开之前的文件
if(mode==0||res==0x04)res=f_open(f_bmp (const TCHAR*)filename FA_WRITE|
FA_CREATE_NEW);//模式 0 或者尝试打开失败 则创建新文件
if((hbmp.bmiHeader.biWidth*2)%4)//水平像素(字节)不为 4 的倍数
{
bi4width=((hbmp.bmiHeader.biWidth*2)/4 1)*4;//实际像素 必须为 4 的倍数.
}else bi4width=hbmp.bmiHeader.biWidth*2;
//刚好为 4 的倍数
if(res==FR_OK)//创建成功
{
res=f_write(f_bmp (u8*)&hbmp bmpheadsize &bw);//写入 BMP 首部
for(ty=y height-1;hbmp.bmiHeader.biHeight;ty--)
{
pixcnt=0;
for(tx=x;pixcnt!=(bi4width/2);)
{
if(pixcnt<hbmp.bmiHeader.biWidth)databuf[pixcnt]=LCD_ReadPoint(tx ty);
//读取坐标点的值
else databuf[pixcnt]=0Xffff;//补充白色的像素.
pixcnt ; tx ;
}
hbmp.bmiHeader.biHeight--;
res=f_write(f_bmp (u8*)databuf bi4width &bw);//写入数据
}
f_close(f_bmp);
}
#if BMP_USE_MALLOC == 1 //使用 malloc
pic_memfree(databuf); pic_memfree(f_bmp);
#endif
return res;
}
该函数实现了对 LCD 屏幕的任意指定区域进行截屏保存,用到的方法就是 51.1.1 节我们所
介绍的方法,该函数实现了将 LCD 任意指定区域的内容,保存个为 16 位 BMP 格式,存放在
指定位置(由 filename 决定)。注意,代码中的 BMP_USE_MALLOC 是在 bmp.h 定义的一个宏,
用于设置是否使用 malloc,本章我们选择使用 malloc。
最后我们来看看 main.c 文件源码:
//处理 JPEG 数据
//当采集完一帧 JPEG 数据后 调用此函数 切换 JPEG BUF.开始下一帧采集.
void jpeg_data_process(void)
{
u16 i;
u16 rlen;
//剩余数据长度
u32 *pbuf;
curline=yoffset;
//行数复位
if(ovx_mode&0X01)
//只有在 JPEG 格式下 才需要做处理.
{
if(jpeg_data_ok==0)
//jpeg 数据还未采集完?
{
DMA2_Stream1->CR&=~(1<<0);
//停止当前传输
while(DMA2_Stream1->CR&0X01); //等待 DMA2_Stream1 可配置
rlen=jpeg_line_size-DMA2_Stream1->NDTR;//得到剩余数据长度
pbuf=jpeg_data_buf jpeg_data_len;//偏移到有效数据末尾 继续添加
if(DMA2_Stream1->CR&(1<<19))for(i=0;i<rlen;i )pbuf[i]=dcmi_line_buf[1][i];
//读取 buf1 里面的剩余数据
else for(i=0;i<rlen;i )pbuf[i]=dcmi_line_buf[0][i];//读取 buf0 里面的剩余数据
jpeg_data_len =rlen;
//加上剩余长度
jpeg_data_ok=1;
//标记 JPEG 数据采集完按成 等待其他函数处理
}
if(jpeg_data_ok==2)
//上一次的 jpeg 数据已经被处理了
{
DMA2_Stream1->NDTR=jpeg_line_size;//传输长度为 jpeg_buf_size*4 字节
DMA2_Stream1->CR|=1<<0;
//重新传输
jpeg_data_ok=0;
//标记数据未采集
jpeg_data_len=0;
//数据重新开始
}
}else
{
if(bmp_request==1)
//有 bmp 拍照请求 关闭 DCMI
{
DCMI_Stop();//停止 DCMI
bmp_request=0;
//标记请求处理完成.
}
LCD_SetCursor(0 0);
LCD_WriteRAM_Prepare(); //开始写入 GRAM
}
}
//jpeg 数据接收回调函数
void jpeg_dcmi_rx_callback(void)
{
u16 i;
u32 *pbuf;
pbuf=jpeg_data_buf jpeg_data_len;//偏移到有效数据末尾
if(DMA2_Stream1->CR&(1<<19))//buf0 已满 正常处理 buf1
{
for(i=0;i<jpeg_line_size;i )pbuf[i]=dcmi_line_buf[0][i];//读取 buf0 里面的数据
jpeg_data_len =jpeg_line_size;//偏移
}else //buf1 已满 正常处理 buf0
{
for(i=0;i<jpeg_line_size;i )pbuf[i]=dcmi_line_buf[1][i];//读取 buf1 里面的数据
jpeg_data_len =jpeg_line_size;//偏移
}
}
//RGB 屏数据接收回调函数
void rgblcd_dcmi_rx_callback(void)
{
u16 *pbuf;
if(DMA2_Stream1->CR&(1<<19))//DMA 使用 buf1 读取 buf0
{
pbuf=(u16*)dcmi_line_buf[0];
}else
//DMA 使用 buf0 读取 buf1
{
pbuf=(u16*)dcmi_line_buf[1];
}
LTDC_Color_Fill(0 curline lcddev.width-1 curline pbuf);//DM2D 填充
if(curline<lcddev.height)curline ;
if(bmp_request==1&&curline==(lcddev.height-1))//有 bmp 拍照请求 关闭 DCMI
{
DCMI_Stop();//停止 DCMI
bmp_request=0;
//标记请求处理完成.
}
}
//切换为 OV5640 模式
void sw_ov5640_mode(void)
{
GPIO_InitTypeDef GPIO_Initure;
OV5640_WR_Reg(0X3017 0XFF);
//开启 OV5650 输出(可以正常显示)
OV5640_WR_Reg(0X3018 0XFF);
//GPIOC8/9/11 切换为 DCMI 接口
GPIO_Initure.Pin=GPIO_PIN_8|GPIO_PIN_9|GPIO_PIN_11;
GPIO_Initure.Mode=GPIO_MODE_AF_PP;
//推挽复用
GPIO_Initure.Pull=GPIO_PULLUP; //上拉
GPIO_Initure.Speed=GPIO_SPEED_HIGH; //高速
GPIO_Initure.Alternate=GPIO_AF13_DCMI; //复用为 DCMI
HAL_GPIO_Init(GPIOC &GPIO_Initure); //初始化
}
//切换为 SD 卡模式
void sw_sdcard_mode(void)
{
GPIO_InitTypeDef GPIO_Initure;
OV5640_WR_Reg(0X3017 0X00);
//关闭 OV5640 全部输出(不影响 SD 卡通信)
OV5640_WR_Reg(0X3018 0X00);
//GPIOC8/9/11 切换为 SDIO 接口
GPIO_Initure.Pin=GPIO_PIN_8|GPIO_PIN_9|GPIO_PIN_11;
GPIO_Initure.Mode=GPIO_MODE_AF_PP;
//推挽复用
GPIO_Initure.Pull=GPIO_PULLUP;
//上拉
GPIO_Initure.Speed=GPIO_SPEED_HIGH; //高速
GPIO_Initure.Alternate=GPIO_AF12_SDMMC1; //复用为 SDIO
HAL_GPIO_Init(GPIOC &GPIO_Initure);
}
//文件名自增(避免覆盖)
//mode:0 创建.bmp 文件;1 创建.jpg 文件.
//bmp 组合成:形如"0:PHOTO/PIC13141.bmp"的文件名
//jpg 组合成:形如"0:PHOTO/PIC13141.jpg"的文件名
void camera_new_pathname(u8 *pname u8 mode)
{
……//省略部分代码
}
//OV5640 拍照 jpg 图片
//返回值:0 成功
// 其他 错误代码
u8 ov5640_jpg_photo(u8 *pname)
{
FIL* f_jpg;
u8 res=0 headok=0;
u32 bwr;
u32 i jpgstart jpglen;
u8* pbuf;
f_jpg=(FIL *)mymalloc(SRAMIN sizeof(FIL)); //开辟 FIL 字节的内存区域
if(f_jpg==NULL)return 0XFF;
//内存申请失败.
ovx_mode=1;
jpeg_data_ok=0;
sw_ov5640_mode();
//切换为 OV5640 模式
OV5640_JPEG_Mode();
//JPEG 模式
OV5640_OutSize_Set(16 4 2592 1944);
//设置输出尺寸(500W)
dcmi_rx_callback=jpeg_dcmi_rx_callback; //JPEG 接收数据回调函数
DCMI_DMA_Init((u32)dcmi_line_buf[0] (u32)dcmi_line_buf[1] jpeg_line_size
DMA_MDATAALIGN_WORD DMA_MINC_ENABLE);//DCMI DMA 配置
DCMI_Start();
//启动传输
while(jpeg_data_ok!=1);
//等待第一帧图片采集完
jpeg_data_ok=2;
//忽略本帧图片 启动下一帧采集
while(jpeg_data_ok!=1);
//等待第二帧图片采集完 第二帧 才保存到 SD 卡去.
DCMI_Stop();
//停止 DMA 搬运
ovx_mode=0;
sw_sdcard_mode();
//切换为 SD 卡模式
res=f_open(f_jpg (const TCHAR*)pname FA_WRITE|FA_CREATE_NEW);
//模式 0 或者尝试打开失败 则创建新文件
if(res==0)
{
printf("jpeg data size:%d\r\n" jpeg_data_len*4);//串口打印 JPEG 文件大小
pbuf=(u8*)jpeg_data_buf;
jpglen=0; //设置 jpg 文件大小为 0
headok=0;
//清除 jpg 头标记
for(i=0;i<jpeg_data_len*4;i )//查找 0XFF 0XD8 和 0XFF 0XD9 获取 jpg 文件大小
{
if((pbuf[i]==0XFF)&&(pbuf[i 1]==0XD8))//找到 FF D8
{
jpgstart=i;
headok=1;
//标记找到 jpg 头(FF D8)
}
if((pbuf[i]==0XFF)&&(pbuf[i 1]==0XD9)&&headok)//找到头以后 再找 FF D9
{
jpglen=i-jpgstart 2;
break;
}
}
if(jpglen)
//正常的 jpeg 数据
{
pbuf =jpgstart;
//偏移到 0XFF 0XD8 处
res=f_write(f_jpg pbuf jpglen &bwr);
if(bwr!=jpglen)res=0XFE;
}else res=0XFD;
}
jpeg_data_len=0;
f_close(f_jpg);
sw_ov5640_mode();
//切换为 OV5640 模式
OV5640_RGB565_Mode(); //RGB565 模式
if(lcdltdc.pwidth!=0)
//RGB 屏
{
dcmi_rx_callback=rgblcd_dcmi_rx_callback;//RGB 屏接收数据回调函数
DCMI_DMA_Init((u32)dcmi_line_buf[0] (u32)dcmi_line_buf[1] lcddev.width/2
DMA_MDATAALIGN_HALFWORD DMA_MINC_ENABLE);//DCMI DMA 配置
}else
//MCU 屏
{
DCMI_DMA_Init((u32)&LCD->LCD_RAM 0 1 DMA_MDATAALIGN_HALFWORD
DMA_MINC_DISABLE); //DCMI DMA 配置 MCU 屏 竖屏
}
myfree(SRAMIN f_jpg);
return res;
}
int main(void)
{
u8 res fac;
u8 *pname;
//带路径的文件名
u8 key;
//键值
u8 i;
u8 sd_ok=1;
//0 sd 卡不正常;1 SD 卡正常.
u8 scale=1;
//默认是全尺寸缩放
u8 msgbuf[15];
//消息缓存区
u16 outputheight=0;
Cache_Enable(); //打开 L1-Cache
MPU_Memory_Protection(); //保护相关存储区域
HAL_Init();
//初始化 HAL 库
Stm32_Clock_Init(432 25 2 9); //设置时钟 216Mhz
delay_init(216); //延时初始化
uart_init(115200);
//串口初始化
usmart_dev.init(108);
//初始化 USMART
LED_Init();
//初始化 LED
KEY_Init();
……//省略部分代码
res=f_mkdir("0:/PHOTO");
//创建 PHOTO 文件夹
if(res!=FR_EXIST&&res!=FR_OK) //发生了错误
{
res=f_mkdir("0:/PHOTO");
//创建 PHOTO 文件夹
Show_Str(30 190 240 16 "SD 卡错误!" 16 0);
delay_ms(200);
Show_Str(30 190 240 16 "拍照功能将不可用!" 16 0);
delay_ms(200);
sd_ok=0;
}
dcmi_line_buf[0]=mymalloc(SRAMIN jpeg_line_size*4); //为 jpeg dma 接收申请内存
dcmi_line_buf[1]=mymalloc(SRAMIN jpeg_line_size*4); //为 jpeg dma 接收申请内存
jpeg_data_buf=mymalloc(SRAMEX jpeg_buf_size);
//为 jpeg 申请内存(最大 4MB)
pname=mymalloc(SRAMIN 30);//为带路径的文件名分配 30 个字节的内存
while(pname==NULL||!dcmi_line_buf[0]||!dcmi_line_buf[1]||!jpeg_data_buf) //分配出错
{
Show_Str(30 190 240 16 "内存分配失败!" 16 0);
delay_ms(200);
LCD_Fill(30 190 240 146 WHITE);//清除显示
delay_ms(200);
}
while(OV5640_Init())//初始化 OV5640
{
Show_Str(30 190 240 16 "OV5640 错误!" 16 0);
delay_ms(200);
LCD_Fill(30 190 239 206 WHITE);
delay_ms(200);
}
Show_Str(30 210 230 16 "OV5640 正常" 16 0);
//自动对焦初始化
OV5640_RGB565_Mode(); //RGB565 模式
OV5640_Focus_Init();
OV5640_Light_Mode(0);
//自动模式
OV5640_Color_Saturation(3);//色彩饱和度 0
OV5640_Brightness(4); //亮度 0
OV5640_Contrast(3);
//对比度 0
OV5640_Sharpness(33);//自动锐度
OV5640_Focus_Constant();//启动持续对焦
DCMI_Init();
//DCMI 配置
if(lcdltdc.pwidth!=0)
//RGB 屏
{
dcmi_rx_callback=rgblcd_dcmi_rx_callback;//RGB 屏接收数据回调函数
DCMI_DMA_Init((u32)dcmi_line_buf[0] (u32)dcmi_line_buf[1] lcddev.width/2
DMA_MDATAALIGN_HALFWORD DMA_MINC_ENABLE);//DCMI DMA 配置
}else
//MCU 屏
{
DCMI_DMA_Init((u32)&LCD->LCD_RAM 0 1 DMA_MDATAALIGN_HALFWORD
DMA_MINC_DISABLE); //DCMI DMA 配置 MCU 屏 竖屏
}
if(lcddev.height>800)
{
yoffset=(lcddev.height-800)/2;
outputheight=800;
OV5640_WR_Reg(0x3035 0X51);//降低输出帧率,否则可能抖动
}else
{
yoffset=0;
outputheight=lcddev.height;
}
curline=yoffset;
//行数复位
OV5640_OutSize_Set(16 4 lcddev.width outputheight);
//满屏缩放显示
DCMI_Start();
//启动传输
LCD_Clear(BLACK);
while(1)
{
key=KEY_Scan(0);//不支持连按
if(key)
{
if(key!=KEY2_PRES)
{
if(key==KEY0_PRES)//BMP 拍照则等待 1 秒去抖以获得稳定 bmp 照片
{
delay_ms(300);
bmp_request=1;
//请求关闭 DCMI
while(bmp_request);
//等带请求处理完成
}else DCMI_Stop();
}
if(key==WKUP_PRES)
//缩放处理
{
scale=!scale;
if(scale==0)
{
fac=800/outputheight;//得到比例因子
OV5640_OutSize_Set((1280-fac*lcddev.width)/2
(800-fac*outputheight)/2 lcddev.width outputheight);
sprintf((char*)msgbuf "Full Size 1:1");
}else
{
OV5640_OutSize_Set(16 4 lcddev.width outputheight);
sprintf((char*)msgbuf "Scale");
}
delay_ms(800);
}else if(key==KEY2_PRES)OV5640_Focus_Single(); //手动单次自动对焦
else if(sd_ok)//SD 卡正常才可以拍照
{
sw_sdcard_mode();//切换为 SD 卡模式
if(key==KEY0_PRES) //BMP 拍照
{
camera_new_pathname(pname 0); //得到文件名
res=bmp_encode(pname 0 yoffset lcddev.width outputheight 0);
sw_ov5640_mode();
//切换为 OV5640 模式
}else if(key==KEY1_PRES)//JPG 拍照
{
camera_new_pathname(pname 1);//得到文件名
res=ov5640_jpg_photo(pname);
if(scale==0)
{
fac=800/lcddev.height;//得到比例因子
OV5640_OutSize_Set((1280-fac*lcddev.width)/2
(800-fac*lcddev.height)/2 lcddev.width lcddev.height);
}else
{
OV5640_OutSize_Set(16 4 lcddev.width outputheight);
}
if(lcddev.height>800)OV5640_WR_Reg(0x3035 0X51);//降低输出帧率防抖
}
if(res)//拍照有误
{
Show_Str(30 130 240 16 "写入文件错误!" 16 0);
}else
{
Show_Str(30 130 240 16 "拍照成功!" 16 0);
Show_Str(30 150 240 16 "保存为:" 16 0);
Show_Str(30 56 150 240 16 pname 16 0);
BEEP(0);
//蜂鸣器短叫,提示拍照完成
delay_ms(100);
BEEP(1);
//关闭蜂鸣器
}
delay_ms(1000);
//等待 1 秒钟
DCMI_Start();//先使能 dcmi 然后关闭面再开 DCMI 防止 RGB 屏侧移
DCMI_Stop();
}else //提示 SD 卡错误
{
Show_Str(30 130 240 16 "SD 卡错误!" 16 0);
Show_Str(30 150 240 16 "拍照功能不可用!" 16 0);
}
if(key!=KEY2_PRES)DCMI_Start();//开始显示
}
delay_ms(10);
i ;
if(i==20)//DS0 闪烁.
{
i=0;
LED0_Toggle;
}
}
}
这部分代码比较长,我们省略了一些内容。详细的代码,请大家参考光盘本例程源码。在
main.c 里面,总共有 8 个函数,我们接下来分别介绍。
1,jpeg_data_process 函数
该函数用于处理 JPEG 数据的接收,在 DCMI_IRQHandler 函数(在 dcmi.c 里面)里面被
调用,它与 jpeg_dcmi_rx_callback 函数和 ov5640_jpg_photo 函数共同控制 JPEG 的数据的采集。
JPEG 数据的接收,采用 DMA 双缓冲机制,缓冲数组为:dcmi_line_buf(u32 类型,RGB 屏接
收 RGB565 数据时,也是用这个数组);数组大小为:jpeg_line_size,我们定义的是 2*1024,
即数组大小为 8K 字节(数组大小不能小于存储摄像头一行输出数据的大小);JPEG 数据接收
处理流程就是按图 51.1.2.1 所示流程来实现的。由 DMA 传输完成中断和 DCMI 帧中断,两个
中断服务函数共同完成 jpeg 数据的采集。采集到的 JPEG 数据,全部存储在 jpeg_data_buf 数组
里面,jpeg_data_buf 数组采用内存管理,从外部 SDRAM 申请 4MB 内存作为 JPEG 数据的缓存。
2,jpeg_dcmi_rx_callback 函数
这是 jpeg 数据接收的主要函数,通过判断 DMA2_Stream1->CR 寄存器,读取不同
dcmi_line_buf 里面的数据,存储到 SDRAM 里面(jpeg_data_buf)。该函数由 DMA 的传输完
成中断服务函数:DMA2_Stream1_IRQHandler 调用。
3,rgblcd_dcmi_rx_callback 函数
该函数仅在使用 RGB 屏的时候用到。当使用 RGB 屏的时候,我们每接收一行数据,就使
用 DMA2D 填充到 RGB 屏的 GRAM,这里同样是使用 DMA 的双缓冲机制来接收 RGB565 数
据,原理参照图 51.1.2.1。该函数由 DMA 传输完成中断服务函数调用。
4,sw_ov5640_mode
因为 SD 卡和 OV5640 有几个 IO 共用,所以这几个 IO 需要分时复用。该函数用于切换
GPIO8/9/11 的复用功能为 DCMI 接口,并开启 OV5640,这样摄像头模块,可以开始正常工作。
5,sw_sdcard_mode
该数用于切换 GPIO8/9/11 的复用功能为 SDMMC 接口,并关闭 OV5640,这样,SD 卡可
以开始正常工作。
6,camera_new_pathname 函数
该函数用于生成新的带路径的文件名,且不会重复,防止文件互相覆盖。该函数可以生
成.bmp/.jpg 的文件名,方便拍照的时候,保存到 SD 卡里面。
7,ov5640_jpg_photo 函数
该函数实现 OV5640 的 JPEG 图像采集,并保存图像到 SD 卡,完成 JPEG 拍照。该函数首
先设置 OV5640 工作在 JPEG 模式,然后,设置输出分辨率为最高的 QSXGA(2592*1944)。然
后,开始采集 JPEG 数据,将第二帧 JPEG 数据,保留下来,并写入 SD 卡里面,完成一次 JPEG
拍照。这里,我们丢弃第一帧 JPEG 数据,是防止采集到的图像数据不完整,导致图片错误。
另外,在保存 jpeg 图片的时候,我们将 0XFF 0XD8 和 0XFF 0XD9 之外的数据,进行了剔
除,只留下 0XFF 0XD8~0XFF 0XD9 之间的数据,保证图片文件最小,且无其他乱的数据。
注意,在保存图片的时候,必须将 PC8/9/11 切换为 SD 卡模式,并关闭 OV5640 的输出。
在图片保存完成以后,切换回 OV5640 模式,并重新使能 OV5640 的输出。
8,main 函数
该函数完成对各相关硬件的初始化,然后检测 OV5640,初始化 OV5640 位 RGB565 模式,显示采集到的图像到 LCD 上面,实现对图像进行预览。进入主循环以后,按 KEY0 按键,可
以实现 BMP 拍照(实际上就是截屏,通过 bmp_encode 函数实现);按 KEY1 按键,可实现 JPEG
拍照(2592*1944 分辨率,通过 ov5640_jpg_photo 函数实现);按 KEY2 按键,可以实现自动
对焦(单次);按 KEY_UP 按键,可以实现图像缩放/不缩放预览。main 函数实现了我们在 51.2
节所提到的功能。
至此照相机实验代码编写完成。最后,本实验可以通过 USMART 来设置 OV5640 的相关
参数,将 OV5640_Contrast、OV5640_Color_Saturation 和 OV5640_Light_Mode 等函数添加到
USMART 管理,即可通过串口设置 OV5640 的参数,方便调试。
49.4 下载验证
在代码编译成功之后,我们通过下载代码到 ALIENTEK 水星 STM32 开发板上,得到如图
49.4.1 所示界面:
图 49.4.1 程序运行效果图
随后,进入监控界面。此时,我们可以按下 KEY0 和 KEY1,即可进行 bmp/jpg 拍照。拍
照得到的照片效果如图 49.4.2 和图 49.4.3 所示:
图 49.4.2 拍照样图(bmp 拍照样图)
图 49.4.3 拍照样图(jpg 拍照样图)
如果发现对焦不清晰,可以按 TPAD 进行一次自动对焦。按 KEY_UP 可以实现缩放/不缩
放显示。