第四十七章 照相机实验
上一章,我们学习了图片解码,本章我们将学习BMP&JPEG编码,结合前面的摄像头实验,实现一个简单的照相机。
本章分为如下几个小节:
47.1 BMP&JPEG编码简介
47.2 硬件设计
47.3 程序设计
47.4 下载验证
47.1 BMP&JPEG编码简介
我们要实现支持BMP图片格式的照片和JPEG图片格式的照片的照相机功能,这里简单介绍一下这两种图片格式的编码。这里我们使用正点原子OV5640-AF摄像头,来实现拍照。关于OV5640的相关知识点,请参考前面的章节。
47.1.1 BMP编码简介
前面的章节中,我们学习了各种图片格式的解码。本章,我们介绍最简单的图片编码方法:BMP图片编码。通过前面的了解,我们知道BMP文件是由文件头、位图信息头、颜色信息和图形数据等四部分组成。我们先来了解下这几个部分。
1、BMP文件头(14字节):BMP文件头数据结构含有BMP文件的类型、文件大小和位图起始位置等信息。
这里的__PACKED_STRUCT是强制对齐,这里是把结构体中间的留白空间移除。默认定义的变量是按CPU字长(STM32为32位)对齐的,这样可以增加程序的访问速度,但这样定义一个结构体时如果结构体成员并不全部按CUP的字长去定义,如有uint8_t,uint16_t时,编译器默认按照的长度将占用2个uint32_t类型的长度,而对于嵌入式产品尤其是内存紧张的产品,这样定义的结构体变量就会浪费内存空间。嵌入式的编译器支持通过强制对齐,可以优化结构体变量的空间,大家同样可以在MDK的帮助文件中查找__packed关键字去看这部分的知识点。我们这里用了在MDK下同时兼容AC5和AC6编译器的写法。BMP的文件头定义如下:
/* BMP头文件 */
typedef __PACKED_STRUCT
{
uint16_t bfType; /* 文件标志.只对'BM',用来识别BMP位图类型 */
uint32_t bfSize; /* 文件大小,占四个字节 */
uint16_t bfReserved1; /* 保留 */
uint16_t bfReserved2; /* 保留 */
uint32_t bfOffBits; /* 从文件开始到位图数据(bitmap data)开始之间的的偏移量 */
}BITMAPFILEHEADER ;
2、位图信息头(40字节):BMP位图信息头数据用于说明位图的尺寸等信息。
/* BMP信息头 */
typedef __PACKED_STRUCT
{
uint32_t biSize; /* 说明BITMAPINFOHEADER结构所需要的字数。 */
long biWidth; /* 说明图象的宽度,以象素为单位 */
long biHeight; /* 说明图象的高度,以象素为单位 */
uint16_t biPlanes; /* 为目标设备说明位面数,其值将总是被设为1 */
uint16_t biBitCount; /* 说明比特数/象素,其值为1、4、8、16、24、或32 */
uint32_t biCompression; /* 说明图象数据压缩的类型。其值可以是下述值之一
* BI_RGB :没有压缩
* BI_RLE8 :每个象素8bit的RLE压缩编码,压缩格式由2B组成
* BI_RLE4 :每个象素4bit的RLE压缩编码,压缩格式由2B组成
* BI_BITFIELDS:每个象素的比特由指定的掩码决定
*/
uint32_t biSizeImage;/*说明图象的大小,以字节为单位。当用BI_RGB格式时,可设置为0*/
long biXPelsPerMeter ; /* 说明水平分辨率,用象素/米表示 */
long biYPelsPerMeter ; /* 说明垂直分辨率,用象素/米表示 */
uint32_t biClrUsed ; /* 说明位图实际使用的彩色表中的颜色索引数 */
/* 说明对图象显示有重要影响的颜色索引的数目,如果是0,表示都重要 */
uint32_t biClrImportant ;
}BITMAPINFOHEADER ;
3、颜色表:颜色表用于说明位图中的颜色,它有若干个表项,每一个表项是一个RGBQUAD类型的结构,定义一种颜色。
/* 彩色表 */
typedef __PACKED_STRUCT
{
uint8_t rgbBlue ; /* 指定蓝色强度 */
uint8_t rgbGreen ; /* 指定绿色强度 */
uint8_t rgbRed ; /* 指定红色强度 */
uint8_t rgbReserved ; /* 保留,设置为0 */
}RGBQUAD ;
颜色表中RGBQUAD结构数据的个数由biBitCount来确定:当biBitCount=1、4、8时,分别有2、16、256个表项;当biBitCount大于8时,没有颜色表项。
BMP文件头、位图信息头和颜色表组成位图信息(我们将BMP文件头也加进来,方便处理),BITMAPINFO结构定义如下:
/* 位图信息头 */
typedef __PACKED_STRUCT
{
BITMAPFILEHEADER bmfHeader;
BITMAPINFOHEADER bmiHeader;
uint32_t RGB_MASK[3]; /* 调色板用于存放RGB掩码 */
//RGBQUAD bmiColors[256];
}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;
uint32_t 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编码就介绍到这里。
47.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拍照了。
47.2 硬件设计
1. 例程功能
1、首先是检测字库,然后检测SD卡根目录是否存在PHOTO文件夹,如果不存在则创建,如果创建失败,则报错(提示拍照功能不可用)。在找到SD卡的PHOTO文件夹后,开始初始化OV5640,如果初始化成功,就一直在屏幕显示OV5640拍到的内容。当按下KEY_UP按键时,可以选择缩放还是1:1显示,默认缩放。按下KEY0,可以拍bmp格式照片(分辨率为LCD分辨率)。按下KEY1可以拍JPEG格式照片(分辨率为QSXGA即2592*1944)。拍照保存成功之后,蜂鸣器会发出“滴”的一声,提示拍照成功。
2、LED0闪烁,提示程序运行。LED1用于指示帧中断。
2. 硬件资源
1)LED灯
LED0 - PB4
LED1 - PB5
2) 蜂鸣器
BEEP - PE0
3)独立按键 :
KEY0 - PA1
KEY1 - PA15
WK_UP - PA0
4)串口1(PA9/PA10连接在板载USB转串口芯片CH340上面)
5)正点原子2.8/3.5/4.3/7寸TFTLCD模块(仅限MCU屏,16位8080并口驱动)
6)SD卡,通过SDMMC1(SDMMC_D0~D4(PC8~PC11),
SDMMC_SCK(PC12),SDMMC_CMD(PD2))连接
7)norflash(QSPI FLASH芯片,连接在OSPI上)
8)定时器4
9)ATK-MC5640摄像头模块
47.3 程序设计
47.3.1 程序流程图

图47.3.1.1 照相机实验程序流程图
47.3.2 程序解析
1. PICTURE驱动代码
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。PICTURE驱动源码包括两个文件:bmp.c和bmp.h。
bmp.h头文件在47.1.1小节基本讲过,具体请看源码。下面来看到bmp.c文件里面的bmp编码函数:bmp_encode,该函数代码如下:
/**
* @brief BMP编码函数
* @note 将当前LCD屏幕的指定区域截图,存为16位格式的BMP文件 RGB565格式.
* 保存为rgb565则需要掩码,需要利用原来的调色板位置增加掩码.这里我们已经增加了掩码.
* 保存为rgb555格式则需要颜色转换,耗时间比较久,所以保存为565是最快速的办法.
*
* @param filename : 包含存储路径的文件名(.bmp)
* @param x, y : 起始坐标
* @param width,height: 区域大小
* @param acolor : 附加的alphablend的颜色(这个仅对32位色 bmp有效!!!)
* @param mode : 保存模式
* @arg 0, 仅仅创建新文件的方式编码;
* @arg 1, 如果之前存在文件,则覆盖之前的文件.如果没有,则创建新的文件;
* @retval 操作结果
* @arg 0 , 成功
* @arg 其他, 错误码
*/
uint8_t bmp_encode(uint8_t *filename, uint16_t x, uint16_t y, uint16_t width,
uint16_t height, uint8_t mode)
{
FIL *f_bmp;
uint32_t bw = 0;
uint16_t bmpheadsize; /* bmp头大小 */
BITMAPINFO hbmp; /* bmp头 */
uint8_t res = 0;
uint16_t tx, ty; /* 图像尺寸 */
uint16_t *databuf; /* 数据缓存区地址 */
uint16_t pixcnt; /* 像素计数器 */
uint16_t 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 */
/* 开辟至少bi4width大小的字节的内存区域 ,对240宽的屏,480个字节就够了.
最大支持1024宽度的bmp编码 */
databuf = (uint16_t *)piclib_mem_malloc(2048);
if (databuf == NULL)return PIC_MEM_ERR; /* 内存申请失败. */
f_bmp = (FIL *)piclib_mem_malloc(sizeof(FIL)); /* 开辟FIL字节的内存区域 */
if (f_bmp == NULL) /* 内存申请失败 */
{
piclib_mem_free(databuf);
return PIC_MEM_ERR;
}
#else
databuf = (uint16_t *)bmpreadbuf;
f_bmp = &f_bfile;
#endif
bmpheadsize = sizeof(hbmp); /* 得到bmp文件头的大小 */
my_mem_set((uint8_t *)&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 = ((uint16_t)'M' << 8) + 'B'; /* BM格式标志 */
/* 整个bmp的大小 */
hbmp.bmfHeader.bfSize = bmpheadsize + hbmp.bmiHeader.biSizeImage;
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)
{ /* 模式0,或者尝试打开失败,则创建新文件 */
res = f_open(f_bmp, (const TCHAR *)filename, FA_WRITE | FA_CREATE_NEW);
}
if ((hbmp.bmiHeader.biWidth * 2) % 4) /* 水平像素(字节)不为4的倍数 */
{ /* 实际要写入的宽度像素,必须为4的倍数 */
bi4width = ((hbmp.bmiHeader.biWidth * 2) / 4 + 1) * 4;
}
else
{
bi4width = hbmp.bmiHeader.biWidth * 2; /* 刚好为4的倍数 */
}
if (res == FR_OK) /* 创建成功 */
{
res = f_write(f_bmp, (uint8_t *)&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] = pic_phy.read_point(tx, ty);/* 读取坐标点的值 */
}
else
{
databuf[pixcnt] = 0Xffff; /* 补充白色的像素 */
}
pixcnt++;
tx++;
}
hbmp.bmiHeader.biHeight--;
res = f_write(f_bmp, (uint8_t *)databuf, bi4width, &bw);/* 写入数据 */
}
f_close(f_bmp);
}
#if BMP_USE_MALLOC == 1 /* 使用malloc */
piclib_mem_free(databuf);
piclib_mem_free(f_bmp);
#endif
return res;
}
该函数实现了对LCD屏幕的任意指定区域进行截屏保存,用到的方法就是47.1.1节我们所介绍的方法,该函数实现了将LCD任意指定区域的内容,保存个为16位BMP格式,存放在指定位置(由filename决定)。注意,代码中的BMP_USE_MALLOC是在bmp.h定义的一个宏,用于设置是否使用malloc,本章我们选择使用malloc。
2. main.c代码
main.c前面定义了一些变量和数组,具体如下:
/* JPEG数据接收缓冲区区域大小(最大65535字节),单位:字 */ #define JPEG_BUF_REGION_SIZE (15918) #define JPEG_BUF_REGION_NUM (DMA_NODE_NUM) /* JPEG数据接收缓冲区区域数量 */ /* OV5640输出数据格式 * 0: RGB565格式 * 1: JPEG格式 */ static uint8_t g_ov_mode; static uint16_t g_offset_line; /* LCD显示起始偏移行 */ static uint8_t g_bmp_request = 0; /* BMP拍照请求标志 */ static uint32_t g_jpeg_data_buf[JPEG_BUF_REGION_NUM][JPEG_BUF_REGION_SIZE] __attribute__((aligned(4))); /* JPEG数据缓冲区 */ static uint32_t g_jpeg_data_recv_len = 0; /* JPEG数据缓冲区中有效数据的长度 */ /* JPEG数据状态 * 0: 未采集完成 * 1: 采集完成 */ static uint8_t g_jpeg_data_sta = 0;
在main.c里面,总共有7个函数,我们接下来分别介绍。首先是DCMI帧捕获完成回调函数,其定义如下:
/**
* @brief DCMI帧捕获完成回调函数
* @param hdcmi: DCMI句柄指针
* @param node: 正在传输的DMA链表节点编号
* @retval 无
*/
void dcmi_frame_transfer_complete_user_cb(DCMI_HandleTypeDef *hdcmi,
uint8_t node)
{
UNUSED(node);
LED1_TOGGLE();
if (g_ov_mode != 0)
{
/* 停止DCMI捕获 */
dcmi_stop();
/* 更新采集到的JPEG数据量 */
g_jpeg_data_recv_len += (JPEG_BUF_REGION_SIZE –
(__HAL_DMA_GET_COUNTER(hdcmi->DMA_Handle) / sizeof(uint32_t)));
/* 标记JPEG数据采集完成 */
g_jpeg_data_sta = 1;
}
else
{
/* 处理BMP拍照请求 */
if (g_bmp_request != 0)
{
g_bmp_request = 0;
dcmi_stop();
}
/* 更新LCD显示位置 */
lcd_set_cursor(0, g_offset_line);
lcd_write_ram_prepare();
}
}
该函数用于DCMI帧捕获完成相关代码,包括:LED1翻转;如果是JPEG格式,停止DCMI捕获、更新采集到的JPEG数据量、标记JPEG数据采集完成;如果是RGB565格式,更新LCD显示位置,并处理BMP拍照请求。
接下来介绍的是DMA传输完成用户回调函数,其定义如下:
/**
* @brief DMA传输完成用户回调函数
* @param node: 传输完成的DMA链表节点编号
* @retval 无
*/
void dcmi_dma_transfer_complete_user_cb(uint8_t node)
{
if (g_ov_mode != 0)
{
/* 更新采集到的JPEG数据量 */
g_jpeg_data_recv_len += JPEG_BUF_REGION_SIZE;
}
}
该函数用于更新采集到的JPEG数据量。
接下来介绍的是切换为OV5640模式函数,其定义如下:
/**
* @bref 切换为OV5640模式
* @param 无
* @retval 无
*/
static void sw_ov5640_mode(void)
{
GPIO_InitTypeDef gpio_init_struct;
/* 开启OV5650输出 */
ov5640_write_reg(0x3017, 0xFF);
ov5640_write_reg(0x3018, 0xFF);
/* PC8/9/11复用为DCMI功能 */
gpio_init_struct.Pin = GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_11;
gpio_init_struct.Mode = GPIO_MODE_AF_PP;
gpio_init_struct.Pull = GPIO_PULLUP;
gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;
gpio_init_struct.Alternate = GPIO_AF13_DCMI;
HAL_GPIO_DeInit(GPIOC, GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_11);
HAL_GPIO_Init(GPIOC, &gpio_init_struct);
}
因为SD卡和OV5640有几个IO共用,所以这几个IO需要分时复用。该函数用于切换GPIO8/9/11的复用功能为DCMI接口,并开启OV5640,这样摄像头模块,可以开始正常工作。
接下来介绍的是切换为SD卡模式函数,其定义如下:
/**
* @bref 切换为SD卡模式
* @param 无
* @retval 无
*/
static void sw_sdcard_mode(void)
{
GPIO_InitTypeDef gpio_init_struct;
/* 关闭OV5650输出 */
ov5640_write_reg(0x3017, 0x00);
ov5640_write_reg(0x3018, 0x00);
/* PC8/9/11复用为SDMMC功能 */
gpio_init_struct.Pin = GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_11;
gpio_init_struct.Mode = GPIO_MODE_AF_PP;
gpio_init_struct.Pull = GPIO_PULLUP;
gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;
gpio_init_struct.Alternate = GPIO_AF12_SDMMC1;
HAL_GPIO_DeInit(GPIOC, GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_11);
HAL_GPIO_Init(GPIOC, &gpio_init_struct);
}
该数用于切换GPIO8/9/11的复用功能为SDMMC接口,并关闭OV5640,这样,SD卡可以开始正常工作。
接下来介绍的是生成新照片文件名函数,其定义如下:
/**
* @bref 生成新照片文件名
* @param fname: 文件名
* @param mode: 照片格式
* @arg 0: BMP
* @arg 1: JPG
* @retval 无
*/
static void camera_new_pathname(char *fname, uint8_t mode)
{
FIL *f = NULL;
uint16_t index = 0;
FRESULT res;
/* 申请内存 */
f = (FIL *)mymalloc(SRAMIN, sizeof(FIL));
if (f == NULL)
{
return;
}
while (index < UINT16_MAX)
{
if (mode == 0)
{
sprintf(fname, "0:PHOTO/PIC%05d.bmp", index);
}
else
{
sprintf(fname, "0:PHOTO/PIC%05d.jpg", index);
}
/* 尝试打开文件 */
res = f_open(f, fname, FA_READ);
if (res == FR_NO_FILE)
{
/* 文件不存在,则文件名为新照片文件名 */
break;
}
index++;
}
/* 释放内存 */
myfree(SRAMIN, f);
}
该函数用于生成新的带路径的文件名,且不会重复,防止文件互相覆盖。该函数可以生成.bmp/.jpg的文件名,方便拍照的时候,保存到SD卡里面。
接下来介绍的是OV5640拍照jpg图片函数,其定义如下:
/**
* @bref 保存JPEG图片
* @param fname: 文件名
* @retval 保存结果
* @arg 0: 成功
* @arg 其他: 失败
*/
static uint8_t camera_jpeg_photo(char *fname)
{
FIL *f = NULL;
uint32_t memaddr[JPEG_BUF_REGION_NUM];
uint32_t i;
uint8_t *pbuf;
uint32_t jpeg_start_index;
uint32_t jpeg_data_len = 0;
uint8_t res;
uint32_t bw;
/* 申请内存 */
f = (FIL *)mymalloc(SRAMIN, sizeof(FIL));
if (f == NULL)
{
return 0xFF;
}
g_jpeg_data_sta = 0;
g_jpeg_data_recv_len = 0;
/* 配置OV5640输出JPEG */
g_ov_mode = 1;
sw_ov5640_mode();
ov5640_jpeg_mode();
ov5640_outsize_set(4, 0, 1440, 900);
/* 采集两帧JPEG数据 */
for (i=0; i<(sizeof(memaddr) / sizeof(memaddr[0])); i++)
{
memaddr[i] = (uint32_t)g_jpeg_data_buf[i];
}
dcmi_dma_init(memaddr, JPEG_BUF_REGION_SIZE, sizeof(memaddr)
/ sizeof(memaddr[0]), DMA_DEST_DATAWIDTH_WORD, DMA_DINC_INCREMENTED);
dcmi_start();
while (g_jpeg_data_sta == 0);
g_jpeg_data_sta = 0;
g_jpeg_data_recv_len = 0;
dcmi_dma_init(memaddr, JPEG_BUF_REGION_SIZE, sizeof(memaddr)
/ sizeof(memaddr[0]), DMA_DEST_DATAWIDTH_WORD, DMA_DINC_INCREMENTED);
dcmi_start();
while (g_jpeg_data_sta == 0);
if (g_jpeg_data_recv_len <= (JPEG_BUF_REGION_NUM * JPEG_BUF_REGION_SIZE))
{
/* 寻找JPEG数据的开头(0xFF 0xD8)和结尾(0xFF 0xD9) */
pbuf = (uint8_t *)g_jpeg_data_buf;
jpeg_start_index = JPEG_BUF_REGION_NUM
* JPEG_BUF_REGION_SIZE * sizeof(uint32_t);
for (i=1; i<(g_jpeg_data_recv_len * sizeof(uint32_t)); i++)
{
if (jpeg_start_index == JPEG_BUF_REGION_NUM
* JPEG_BUF_REGION_SIZE * sizeof(uint32_t))
{
if ((pbuf[i - 1] == 0xFF) && (pbuf[i] == 0xD8))
{
jpeg_start_index = i - 1;
}
}
else
{
if ((pbuf[i - 1] == 0xFF) && (pbuf[i] == 0xD9))
{
jpeg_data_len = i - 1 - jpeg_start_index + 2;
break;
}
}
}
}
/* 将有效的JPEG数据写入文件 */
if (jpeg_data_len != 0)
{
sw_sdcard_mode();
res = f_open(f, fname, FA_WRITE | FA_CREATE_NEW);
if (res == FR_OK)
{
res = f_write(f, &pbuf[jpeg_start_index], jpeg_data_len, &bw);
f_close(f);
}
}
else
{
res = 0xFD;
}
/* 配置OV5640输出RGB565 */
g_ov_mode = 0;
sw_ov5640_mode();
ov5640_rgb565_mode();
/* 配置DCMI */
memaddr[0] = (uint32_t)&LCD->LCD_RAM;
dcmi_dma_init(memaddr, UINT16_MAX, 1, DMA_DEST_DATAWIDTH_HALFWORD,
DMA_DINC_FIXED);
myfree(SRAMIN, f);
return res;
}
该函数实现OV5640的JPEG图像采集,并保存图像到SD卡,完成JPEG拍照。该函数首先设置OV5640工作在JPEG模式,然后,设置输出分辨率为WXGA+(1440*900)。然后,开始采集JPEG数据,将第二帧JPEG数据,保留下来,并写入SD卡里面,完成一次JPEG拍照。这里,我们丢弃第一帧JPEG数据,是防止采集到的图像数据不完整,导致图片错误。
另外,在保存jpeg图片的时候,我们将0XFF,0XD8和0XFF,0XD9之外的数据,进行了剔除,只留下0XFF,0XD8~0XFF,0XD9之间的数据,保证图片文件最小,且无其他乱的数据。
注意,在保存图片的时候,必须将PC8/9/11切换为SD卡模式,并关闭OV5640的输出。在图片保存完成以后,切换回OV5640模式,并重新使能OV5640的输出。
最后介绍的是main函数,其定义如下:
int main(void)
{
uint8_t res;
uint8_t sd_ok = 1;
char *fname = NULL;
uint16_t output_height;
uint8_t scale = 1;
float fac;
uint32_t memaddr;
uint8_t t = 0;
uint8_t key;
HAL_Init(); /* HAL库初始化 */
HAL_ICACHE_Enable(); /* 使能指令缓存 */
sys_stm32_clock_init(250, 4, 2, 2); /* 配置时钟,250MHz */
delay_init(250); /* 初始化延时 */
usart_init(115200); /* 初始化串口 */
led_init(); /* 初始化LED */
beep_init(); /* 初始化蜂鸣器 */
key_init(); /* 初始化按键 */
lcd_init(); /* 初始化LCD */
my_mem_init(SRAMIN); /* 初始化内部内存池 */
ov5640_init(); /* 初始化OV5640 */
sw_sdcard_mode(); /* 切换为SD卡模式 */
exfuns_init(); /* 为exfuns申请内存 */
f_mount(fs[0], "0:", 1); /* 挂载SD卡 */
f_mount(fs[1], "1:", 1); /* 挂载NOR Flash */
piclib_init(); /* 初始化画图 */
/* 检查字库 */
while (fonts_init() != 0)
{
lcd_show_string(30, 30, 200, 16, 16, "Font Error! ", RED);
delay_ms(500);
lcd_show_string(30, 30, 200, 16, 16, "Please Check!", RED);
delay_ms(500);
}
lcd_fill(30, 30, 30 + 200, 30 + 16, WHITE);
text_show_string(30, 50, 200, 16, "正点原子STM32开发板",16,0, RED);
text_show_string(30, 70, 200, 16, "照相机实验", 16, 0, RED);
text_show_string(30, 90, 200, 16, "KEY0: 拍照(BMP格式)", 16, 0, RED);
text_show_string(30, 110, 200, 16, "KEY1: 拍照(JPG格式)", 16, 0, RED);
text_show_string(30, 130, 200, 16, "WK_UP: Full/Scale", 16, 0, RED);
/* 创建PHOTO文件夹 */
res = f_mkdir("0:/PHOTO");
if ((res != FR_OK) && (res != FR_EXIST))
{
sd_ok = 0;
text_show_string(30, 150, 200, 16, "SD卡错误!", 16, 0, RED);
text_show_string(30, 170, 200, 16, "拍照功能将不可用!", 16, 0, RED);
delay_ms(500);
}
/* 申请内存 */
fname = (char *)mymalloc(SRAMIN, 30);
while (fname == NULL)
{
text_show_string(30, 190, 200, 16, "内存分配失败!", 16, 0, RED);
delay_ms(500);
lcd_fill(30, 190, 30 + 200, 190 + 16, WHITE);
delay_ms(500);
}
/* 初始化OV5640 */
while (ov5640_init() != 0)
{
text_show_string(30, 190, 200, 16, "OV5640错误!", 16, 0, RED);
delay_ms(500);
lcd_fill(30, 190, 30 + 200, 190 + 16, WHITE);
delay_ms(500);
}
/* 配置OV5640 */
g_ov_mode = 0;
ov5640_rgb565_mode();
ov5640_focus_init();
ov5640_light_mode(0);
ov5640_color_saturation(3);
ov5640_brightness(4);
ov5640_contrast(3);
ov5640_sharpness(33);
ov5640_focus_constant();
if (lcddev.height == 1024)
{
g_offset_line = (lcddev.height - 800) / 2;
output_height = 800;
ov5640_write_reg(0x3035, 0x51);
}
else
{
g_offset_line = 0;
output_height = lcddev.height;
}
ov5640_outsize_set(4, 0, lcddev.width, output_height);
fac = (float)800.f / output_height;
lcd_clear(BLACK);
lcd_set_cursor(0, g_offset_line);
lcd_write_ram_prepare();
/* 配置DCMI */
dcmi_init();
memaddr = (uint32_t)&LCD->LCD_RAM;
dcmi_dma_init(&memaddr, UINT16_MAX, 1, DMA_DEST_DATAWIDTH_HALFWORD,
DMA_DINC_FIXED);
dcmi_start();
while (1)
{
key = key_scan(0);
if ((key == KEY0_PRES) || (key == KEY1_PRES))
{
if (sd_ok == 0)
{
dcmi_stop();
text_show_string(30, 130, 200, 16, "SD卡错误!", 16, 0, RED);
text_show_string(30, 150, 200, 16, "拍照功能不可用!", 16, 0, RED);
}
else
{
if (key == KEY0_PRES)
{
delay_ms(300);
/* 发起BMP拍照请求,并等待请求响应 */
g_bmp_request = 1;
while (g_bmp_request != 0);
/* BMP拍照 */
sw_sdcard_mode();
camera_new_pathname(fname, 0);
res = bmp_encode((uint8_t *)fname, 0, g_offset_line,
lcddev.width, output_height, 0);
}
else if (key == KEY1_PRES)
{
/* JPEG拍照 */
dcmi_stop();
sw_sdcard_mode();
camera_new_pathname(fname, 1);
res = camera_jpeg_photo(fname);
/* 重新配置OV5640 */
if (lcddev.height == 1024)
{
ov5640_write_reg(0x3035, 0x51);
}
if (scale == 0)
{
ov5640_outsize_set((1280 - fac * lcddev.width) / 2, 0,
lcddev.width, output_height);
}
else
{
ov5640_outsize_set(4, 0, lcddev.width, output_height);
}
}
/* 根据拍照结果进行提示 */
if (res == 0)
{
text_show_string(10, 130, 240, 16, "拍照成功!", 16, 0, RED);
text_show_string(10, 150, 240, 16, "保存为:", 16, 0, RED);
text_show_string(10 + 56, 150, 240, 16, (char *)fname,
16, 0, RED);
BEEP(1);
delay_ms(100);
BEEP(0);
}
else
{
text_show_string(10, 130, 240, 16, "写入文件错误!", 16, 0, RED);
}
sw_ov5640_mode();
}
delay_ms(1000);
lcd_set_cursor(0, g_offset_line);
lcd_write_ram_prepare();
dcmi_start();
}
else if (key == WKUP_PRES)
{
/* 切换Full Size和Scale */
scale = !scale;
if (scale == 0)
{
ov5640_outsize_set((1280 - fac * lcddev.width) / 2, 0,
lcddev.width, output_height);
}
else
{
ov5640_outsize_set(4, 0, lcddev.width, output_height);
}
}
if (++t == 20)
{
t = 0;
LED0_TOGGLE();
}
delay_ms(10);
}
}
该函数完成对各相关硬件的初始化,然后检测OV5640,初始化OV5640位RGB565模式,显示采集到的图像到LCD上面,实现对图像进行预览。进入主循环以后,按KEY0按键,可以实现BMP拍照(实际上就是截屏,通过bmp_encode函数实现);按KEY1按键,可实现JPEG拍照(1440*900分辨率,通过ov5640_jpg_photo函数实现);按KEY_UP按键,可以实现图像缩放/不缩放预览。
47.4 下载验证
将程序下载到开发板后,可以看到LCD首先显示一些实验相关的信息,如图47.4.1所示:

图 47 .4.1 显示实验相关信息
显示了上图的信息后,自动进入监控界面。可以看到LED0不停的闪烁,提示程序已经在运行了。此外,LED1不停闪烁,提示进入DCMI中断回调服务函数,进行jpeg数据处理。此时,我们可以按下KEY0和KEY1,即可进行bmp/jpg拍照。拍照得到的照片效果如图47.4.2和图47.4.3所示:

图47.4.2 拍照样图(bmp拍照样图)

图47.4.3 拍照样图(jpg拍照样图)
按KEY_UP可以实现缩放/不缩放显示。
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70040860/viewspace-3106058/ ,如需转载,请注明出处,否则将追究法律责任。