本帖最后由 正点原子运营 于 2023-9-8 18:15 编辑
第五十四章 照相机实验 1)实验平台:正点原子探索者STM32F407开发板
2) 章节摘自【正点原子】STM32F407开发指南 V1.1
3)购买链接:https://detail.tmall.com/item.htm?id=609294673401
4)全套实验源码+手册+视频下载地址:http://www.openedv.com/docs/boards/stm32/zdyz_stm32f407_explorerV3.html
5)正点原子官方B站:https://space.bilibili.com/394620890
6)STM32技术交流QQ群:151941872
上一章,我们学习了如何使用STM32F407图片解码库,实现对JPG/JPEG图片的硬解码,从而大大提高解码速度。本章我们将学习BMP&JPEG编码,结合前面的摄像头实验,实现一个简单的照相机。 本章分为如下几个小节: 54.1 BMP&JPEG编码简介 54.2 硬件设计 54.3 程序设计 54.4 下载验证
54.1BMP&JPEG编码简介我们要实现支持BMP图片格式的照片和JPEG图片格式的照片的照相机功能,这里简单介绍一下这两种图片格式的编码。这里我们使用ATK-OV2640摄像头,来实现拍照。关于OV2640的相关知识点,请参考第四十七章。
54.1.1 BMP编码简介前面的章节中,我们学习了各种图片格式的解码。本章,我们介绍最简单的图片编码方法:BMP图片编码。通过前面的了解,我们知道BMP文件是由文件头、位图信息头、颜色信息和图形数据等四部分组成。我们先来了解下这几个部分。 1、BMP文件头(14字节):BMP文件头数据结构含有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 :每个象素8比特的RLE压缩编码,压缩格式由
- 2字节组成(重复象素计数和颜色索引)
- * BI_RLE4 :每个象素4比特的RLE压缩编码,压缩格式由
- 2字节组成
- * 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编码就介绍到这里。
54.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 OV2640摄像头模块,直接就可以输出压缩后的JPEG数据,我们完全不需要理会压缩过程,所以本实验我们实现JPEG拍照的关键,在于准确接收OV2640摄像头模块发送过来的编码数据,然后将这些数据保存为.jpg文件,就可以实现JPEG拍照了。
在第四十七章的摄像头实验中,我们定义了一个很大的数组jpeg_data_buf(116KB字节)来存储JPEG图像数据。而在本实验中,我们可以使用内存管理来申请内存,无需定义这么大的数组,使用上更加灵活。DCMI接口使用DMA直接传输JPEG数据,DMA接收到的JPEG数据放到内部SRAM。所以,我们本章将使用DMA的双缓冲机制来读取,DMA双缓冲读取JPEG数据框图如图54.1.2.1所示: 图54.1.2.1 DMA双缓冲读取JPEG数据原理框图 DMA接收来自OV2640的JPEG数据流,首先使用M0AR(内存1)来存储,当M0AR满了以后,自动切换到M1AR(内存2),同时程序读取M0AR(内存1)的数据到内部SRAM;当M1AR满了以后,又切回M0AR,同时程序读取M1AR(内存2)的数据到内部SRAM;依次循环(此时的数据处理,是通过DMA传输完成中断实现的,在中断里面处理),直到帧中断,结束一帧数据的采集,读取剩余数据到内部SRAM,完成一次JPEG数据的采集。
这里,M0AR,M1AR所指向的内存,必须是内部内存,不过由于采用了双缓冲机制,我们就不必定义一个很大的数组,一次性接收所有JPEG数据了,而是可以分批次接收,数组可以定义的比较小。
最后,将存储在内部SRAM的jpeg数据,保存为.jpg/.jpeg存放在SD卡,就完成了一次JPEG拍照。
54.2 硬件设计
1. 例程功能1、首先是检测字库,然后检测SD卡根目录是否存在PHOTO文件夹,如果不存在则创建,如果创建失败,则报错(提示拍照功能不可用)。在找到SD卡的PHOTO文件夹后,开始初始化OV2640,如果初始化成功,则提示信息:KEY0:拍照(bmp格式),KEY1:拍照(jpg格式),WK_UP选择:1:1显示,即不缩放,图片不变形,但是显示区域小(液晶分辨率大小),或者缩放显示,即将1280*800的图像压缩到液晶分辨率尺寸显示,图片变形,但是显示了整个图片内容。可以通过串口1,借助USMART设置/读取OV2640的寄存器,方便大家调试。 2、LED0闪烁,提示程序运行。LED1用于指示帧中断。
2. 硬件资源1)LED灯 LED0 – PF9 2)串口1(PA9/PA10连接在板载USB转串口芯片CH340上面) 3)正点原子2.8/3.5/4.3/7寸TFTLCD模块(仅限MCU屏,16位8080并口驱动) 4)独立按键 KEY0 – PE4 KEY1 – PE3 KEY2 – PE2 KEY_UP – PA0 5)SD卡,通过SDIO连接 6)norflash(SPI FLASH芯片,连接在SPI1上) 7)DCMI接口(用于驱动OV2640摄像头模块) 8)定时器6(用于打印摄像头帧率等信息) 9)ALIENTEK OV2640摄像头模块,连接关系为 OV2640模块 ------------ STM32开发板 OV_D0~D7 ------------ PC6/PC7/PC8/PC9/PC11/PB6/PE5/PE6 OV_SCL ------------ PD6 OV_SDA ------------ PD7 OV_VSYNC ------------ PB7 OV_HREF ------------ PA4 OV_PCLK ------------ PA6 OV_PWDN ------------ PG9 OV_RESET ------------ PG15 OV_XCLK ------------ PA8
54.3 程序设计
54.3.1 程序流程图
54.3.2 程序解析
1. PICTURE驱动代码这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。PICTURE驱动源码包括两个文件:bmp.c和bmp.h。
bmp.h头文件在54.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屏幕的任意指定区域进行截屏保存,用到的方法就是54.1.1节我们所介绍的方法,该函数实现了将LCD任意指定区域的内容,保存个为16位BMP格式,存放在指定位置(由filename决定)。注意,代码中的BMP_USE_MALLOC是在bmp.h定义的一个宏,用于设置是否使用malloc,本章我们选择使用malloc。
2. main.c代码main.c前面定义了一些变量和数组,具体如下: - uint8_t g_ov_mode = 0; /* bit0:0,RGB565模式;1,JPEG模式 */
- #define jpeg_buf_size 300*1024 /* 定义JPEG数据缓存jpeg_buf的大小(300K字节) */
- #define jpeg_line_size 1*1024 /* 定义DMA接收数据时,一行数据的最大值 */
- uint32_t *p_dcmi_line_buf[2]; /* JPEG数据 DMA双缓存buf指针 */
- uint32_t *p_jpeg_data_buf; /* JPEG数据缓存buf指针 */
- volatile uint32_t g_jpeg_data_len = 0; /* buf中的JPEG有效数据长度 */
- /**
- * 0,数据没有采集完;
- * 1,数据采集完了,但是还没处理;
- * 2,数据已经处理完成了,可以开始下一帧接收
- */
- volatile uint8_t g_jpeg_data_ok = 0; /* JPEG数据采集完成标志 */
复制代码在main.c里面,总共有7个函数,我们接下来分别介绍。首先是处理JPEG数据函数,其定义如下: - /**
- *@brief 处理JPEG数据
- * @note 当采集完一帧JPEG数据后,调用此函数,切换JPEG BUF.开始下一帧采集
- *@param 无
- *@retval 无
- */
- voidjpeg_data_process(void)
- {
- uint16_t i, rlen;
- uint32_t *pbuf;
- if (g_ov_mode & 0X01) /* 只有在JPEG格式下,才需要做处理 */
- {
- if (g_jpeg_data_ok == 0) /* jpeg数据还未采集完 */
- {
- __HAL_DMA_DISABLE(&g_dma_dcmi_handle); /* 停止当前传输 */
- while(DMA2_Stream1->CR & 0x01); /* 等待DMA2_Stream1可配置 */
- /* 得到剩余数据长度 */
- rlen = jpeg_line_size- __HAL_DMA_GET_COUNTER(&g_dma_dcmi_handle);
- pbuf =p_jpeg_data_buf + g_jpeg_data_len;/* 偏移到有效数据末尾,继续添加 */
- if (DMA1_Stream1->CR & (1 << 19))
- {
- for (i = 0; i < rlen; i++)
- {
- pbuf = p_dcmi_line_buf[1]; /* 读取buf1里面的剩余数据 */
- }
- }
- else
- {
- for (i = 0; i < rlen; i++)
- {
- pbuf = p_dcmi_line_buf[0]; /* 读取buf0里面的剩余数据 */
- }
- }
- g_jpeg_data_len += rlen; /* 加上剩余长度 */
- g_jpeg_data_ok = 1; /* 标记JPEG数据采集完成,等待其他函数处理 */
- }
- if (g_jpeg_data_ok == 2) /* 上一次的jpeg数据已经被处理了 */
- {
- /* 传输长度为jpeg_buf_size*4字节 */
- __HAL_DMA_SET_COUNTER(&g_dma_dcmi_handle, jpeg_line_size);
- __HAL_DMA_ENABLE(&g_dma_dcmi_handle); /* 重新传输 */
- g_jpeg_data_ok = 0; /* 标记数据未采集 */
- g_jpeg_data_len = 0; /* 数据重新开始 */
- }
- }
- }
复制代码该函数用于处理JPEG数据的接收,在DCMI_IRQHandler函数(在dcmi.c里面)里面被调用,它与jpeg_dcmi_rx_callback函数和ov2640_jpg_photo函数共同控制JPEG的数据的采集。JPEG数据的接收,采用DMA双缓冲机制,缓冲数组为:p_dcmi_line_buf(u32类型,RGB屏接收RGB565数据时,也是用这个数组);数组大小为:jpeg_line_size,我们定义的是1*1024,即数组大小为4K字节(数组大小不能小于存储摄像头一行输出数据的大小);JPEG数据接收处理流程就是按图54.1.2.1所示流程来实现的。由DMA传输完成中断和DCMI帧中断,两个中断服务函数共同完成jpeg数据的采集。采集到的JPEG数据,全部存储在p_jpeg_data_buf数组里面,p_jpeg_data_buf数组采用内存管理,从内部SRAM申请300K内存作为JPEG数据的缓存。
接下来介绍的是JPEG数据接收回调函数,其定义如下: - /**
- *@brief JPEG数据接收回调函数
- *@param 无
- *@retval 无
- */
- voidjpeg_dcmi_rx_callback(void)
- {
- uint16_t i;
- volatile uint32_t *pbuf;
- pbuf =p_jpeg_data_buf + g_jpeg_data_len; /* 偏移到有效数据末尾 */
- if (DMA2_Stream1->CR & (1 << 19)) /* buf0已满,正常处理buf1 */
- {
- for (i = 0; i < jpeg_line_size; i++)
- {
- pbuf = p_dcmi_line_buf[0]; /* 读取buf0里面的数据 */
- }
- g_jpeg_data_len += jpeg_line_size; /* 偏移 */
- }
- else /* buf1已满,正常处理buf0 */
- {
- for (i = 0; i < jpeg_line_size; i++)
- {
- pbuf = p_dcmi_line_buf[1]; /* 读取buf1里面的数据 */
- }
- g_jpeg_data_len += jpeg_line_size; /* 偏移 */
- }
- }
复制代码这是jpeg数据接收的主要函数,通过判断DMA2_Stream1->CR寄存器,读取不同p_dcmi_line_buf里面的数据,存储到SRAM里面(p_jpeg_data_buf)。该函数由DMA的传输完成中断服务函数:DMA2_Stream1_IRQHandler调用。
接下来介绍的是切换为OV2640模式函数,其定义如下: - /**
- *@brief 切换为OV2640模式
- * @note 切换PC8/PC9/PC11为DCMI复用功能(AF13)
- *@param 无
- *@retval 无
- */
- void sw_ov2640_mode(void)
- {
- GPIO_InitTypeDef gpio_init_struct;
-
- OV2640_PWDN(0); /* OV2640power up */
- /* GPIOC8/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_VERY_HIGH; /* 高速 */
- gpio_init_struct.Alternate = GPIO_AF13_DCMI; /* 复用为DCMI */
- HAL_GPIO_Init(GPIOC, &gpio_init_struct); /* 初始化PC8,9, 11引脚 */
- }
复制代码因为SD卡和OV2640有几个IO共用,所以这几个IO需要分时复用。该函数用于切换GPIO8/9/11的复用功能为DCMI接口,并开启OV2640,这样摄像头模块,可以开始正常工作。
接下来介绍的是切换为SD卡模式函数,其定义如下: - /**
- *@brief 切换为SD卡模式
- * @note 切换PC8/PC9/PC11为SDMMC复用功能(AF12)
- *@param 无
- *@retval 无
- */
- voidsw_sdcard_mode(void)
- {
- GPIO_InitTypeDef gpio_init_struct;
-
- OV2640_PWDN(1); /* OV2640power down */
-
- /* GPIOC8/9/11切换为 SDIO接口 */
- 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_VERY_HIGH; /* 高速 */
- gpio_init_struct.Alternate = GPIO_AF12_SDIO; /* 复用为SDIO */
- HAL_GPIO_Init(GPIOC, &gpio_init_struct); /* 初始化PC8,9, 11引脚 */
- }
复制代码该数用于切换GPIO8/9/11的复用功能为SDIO接口,并关闭OV2640,这样,SD卡可以开始正常工作。
接下来介绍的是文件名自增(避免覆盖)函数,其定义如下: - /**
- *@brief 文件名自增(避免覆盖)
- * @note bmp组合成: 形如"0:PHOTO/PIC13141.bmp" 的文件名
- * jpg组合成: 形如"0:PHOTO/PIC13141.jpg" 的文件名
- *@param pname : 有效的文件名
- *@param mode : 0, 创建.bmp文件; 1, 创建.jpg文件;
- *@retval 无
- */
- voidcamera_new_pathname(uint8_t *pname, uint8_t mode)
- {
- uint8_t res;
- uint16_t index = 0;
- FIL*ftemp;
- ftemp = (FIL *)mymalloc(SRAMIN, sizeof(FIL)); /* 开辟FIL字节的内存区域 */
- if (ftemp == NULL) return; /* 内存申请失败 */
- while (index < 0XFFFF)
- {
- if (mode == 0) /* 创建.bmp文件名 */
- {
- sprintf((char *)pname, "0:PHOTO/PIC%05d.bmp", index);
- }
- else /* 创建.jpg文件名 */
- {
- sprintf((char *)pname, "0:PHOTO/PIC%05d.jpg", index);
- }
- res = f_open(ftemp, (const TCHAR *)pname, FA_READ); /* 尝试打开这个文件 */
- if (res == FR_NO_FILE)break; /* 该文件名不存在, 正是我们需要的 */
- index++;
- }
- myfree(SRAMIN, ftemp);
- }
复制代码该函数用于生成新的带路径的文件名,且不会重复,防止文件互相覆盖。该函数可以生成.bmp/.jpg的文件名,方便拍照的时候,保存到SD卡里面。
接下来介绍的是OV2640拍照jpg图片函数,其定义如下: - /**
- *@brief OV2640拍照jpg图片
- *@param pname : 要创建的jpg文件名(含路径)
- *@retval 0, 成功; 其他,错误代码;
- */
- uint8_t ov2640_jpg_photo(uint8_t *pname)
- {
- FIL*f_jpg;
- uint8_t res = 0, headok = 0;
- uint32_t bwr;
- uint32_t i, jpgstart, jpglen;
- uint8_t *pbuf;
-
- f_jpg = (FIL *)mymalloc(SRAMIN, sizeof(FIL)); /* 开辟FIL字节的内存区域 */
- if (f_jpg == NULL)return 0XFF; /* 内存申请失败 */
- g_ov_mode = 1;
- g_jpeg_data_ok = 0;
- sw_ov2640_mode(); /* 切换为OV2640模式 */
- ov2640_jpeg_mode(); /* JPEG模式 */
- dcmi_rx_callback = jpeg_dcmi_rx_callback; /* JPEG接收数据回调函数 */
- dcmi_dma_init((uint32_t)p_dcmi_line_buf[0], (uint32_t)p_dcmi_line_buf[1],
- jpeg_line_size, DMA_MDATAALIGN_WORD, DMA_MINC_ENABLE); /* DCMI DMA配置 */
- ov2640_image_win_set(0, 0, 1600, 1200);
- ov2640_outsize_set(1600, 1200); /* 设置输出尺寸(1600 * 1200) */
- dcmi_start(); /* 启动传输 */
- while (g_jpeg_data_ok != 1); /* 等待第一帧图片采集完 */
- g_jpeg_data_ok = 2; /* 忽略本帧图片,启动下一帧采集 */
- while (g_jpeg_data_ok != 1); /* 等待第二帧图片采集完,第二帧,才保存到SD卡去 */
- dcmi_stop(); /* 停止DMA搬运 */
- g_ov_mode = 0;
- sw_sdcard_mode(); /* 切换为SD卡模式 */
- /*串口打印JPEG文件大小 */
- printf("jpegdata size:%d\r\n", g_jpeg_data_len * 4);
- pbuf = (uint8_t *)p_jpeg_data_buf;
- jpglen = 0; /* 设置jpg文件大小为0 */
- headok = 0; /* 清除jpg头标记 */
- /* 查找0XFF,0XD8和0XFF,0XD9,获取jpg文件大小 */
- for (i = 0; i < g_jpeg_data_len * 4; i++)
- {
- if ((pbuf == 0XFF) && (pbuf[i + 1] == 0XD8)) /* 找到FF D8 */
- {
- jpgstart = i;
- headok = 1; /* 标记找到jpg头(FF D8) */
- }
- /* 找到头以后,再找FF D9 */
- if ((pbuf == 0XFF) && (pbuf[i + 1] == 0XD9) && headok)
- {
- jpglen = i - jpgstart + 2;
- break;
- }
- }
- if (jpglen) /* 正常的jpeg数据 */
- {
- /* 模式0,或者尝试打开失败,则创建新文件 */
- res = f_open(f_jpg, (const TCHAR *)pname, FA_WRITE | FA_CREATE_NEW);
- if (res == 0)
- {
- pbuf += jpgstart; /* 偏移到0XFF,0XD8处 */
- res = f_write(f_jpg, pbuf, jpglen, &bwr);
- if (bwr != jpglen)res = 0XFE;
- }
- f_close(f_jpg);
- }
- else
- {
- res = 0XFD;
- }
- g_jpeg_data_len = 0;
- sw_ov2640_mode(); /* 切换为OV2640模式 */
- ov2640_rgb565_mode(); /* RGB565模式 */
- dcmi_dma_init((uint32_t)&LCD->LCD_RAM, 0, 1,DMA_MDATAALIGN_HALFWORD,
- DMA_MINC_DISABLE); /* DCMI DMA配置,MCU屏,竖屏 */
- myfree(SRAMIN, f_jpg);
- return res;
- }
复制代码该函数实现OV2640的JPEG图像采集,并保存图像到SD卡,完成JPEG拍照。该函数首先设置OV2640工作在JPEG模式,然后,设置输出分辨率为UXGA(1600*1200)。然后,开始采集JPEG数据,将第二帧JPEG数据,保留下来,并写入SD卡里面,完成一次JPEG拍照。这里,我们丢弃第一帧JPEG数据,是防止采集到的图像数据不完整,导致图片错误。
另外,在保存jpeg图片的时候,我们将0XFF,0XD8和0XFF,0XD9之外的数据,进行了剔除,只留下0XFF,0XD8~0XFF,0XD9之间的数据,保证图片文件最小,且无其他乱的数据。
注意,在保存图片的时候,必须将PC8/9/11切换为SD卡模式,并关闭OV2640的输出。在图片保存完成以后,切换回OV2640模式,并重新使能OV2640的输出。
最后介绍的是main函数,其定义如下: - int main(void)
- {
- uint8_t res;
- float fac;
- uint8_t *pname; /* 带路径的文件名 */
- uint8_t key; /* 键值 */
- uint8_t i;
- uint8_t sd_ok = 1; /* 0,SD卡不正常;1,SD卡正常 */
- uint8_t scale = 1; /* 默认是全尺寸缩放 */
- uint8_t msgbuf[15]; /* 消息缓存区 */
- HAL_Init(); /* 初始化HAL库 */
- sys_stm32_clock_init(336, 8, 2, 7); /* 设置时钟,168Mhz */
- delay_init(168); /* 延时初始化 */
- usart_init(115200); /* 串口1初始化为115200 */
- usmart_dev.init(84); /* 初始化USMART */
- led_init(); /* 初始化LED */
- beep_init(); /* 初始化蜂鸣器 */
- lcd_init(); /* 初始化LCD */
- key_init(); /* 初始化按键 */
- sram_init(); /* 初始化SRAM */
- btim_timx_int_init(10000 - 1, 8400 - 1); /* 10KHz计数频率,1秒中断一次 */
- piclib_init(); /* 初始化画图 */
- my_mem_init(SRAMIN); /* 初始化内部SRAM内存池 */
- my_mem_init(SRAMEX); /* 初始化外部SRAM内存池 */
- my_mem_init(SRAMCCM); /* 初始化CCM内存池 */
- exfuns_init(); /* 为fatfs相关变量申请内存 */
- f_mount(fs[0], "0:", 1); /* 挂载SD卡 */
- f_mount(fs[1], "1:", 1); /* 挂载FLASH */
- while (fonts_init()) /* 检查字库 */
- {
- lcd_show_string(30, 50, 200, 16, 16, "FontError!", RED);
- delay_ms(200);
- lcd_fill(30, 50, 240, 66, WHITE);/* 清除显示 */
- delay_ms(200);
- }
- while (fonts_init()) /* 检查字库 */
- {
- lcd_show_string(30, 90, 200, 16, 16, "Font error!", RED);
- delay_ms(200);
- lcd_fill(20, 90, 200 + 20, 90 + 16, WHITE);
- delay_ms(200);
- }
- text_show_string(30, 50, 200, 16, "正点原子STM32开发板", 16, 0, RED);
- text_show_string(30, 70, 200, 16, "硬件JPEG解码 实验", 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:FullSize/Scale", 16, 0, RED);
- res= f_mkdir("0:/PHOTO"); /* 创建PHOTO文件夹 */
- if (res != FR_EXIST && res != FR_OK) /* 发生了错误 */
- {
- res = f_mkdir("0:/PHOTO"); /* 创建PHOTO文件夹 */
- text_show_string(30, 150, 240, 16, "SD卡错误!", 16, 0, RED);
- delay_ms(200);
- text_show_string(30, 150, 240, 16, "拍照功能将不可用!", 16, 0, RED);
- delay_ms(200);
- sd_ok = 0;
- }
- /* 为jpeg dma接收申请内存 */
- p_dcmi_line_buf[0] = mymalloc(SRAM12, jpeg_line_size * 4);
- /* 为jpeg dma接收申请内存 */
- p_dcmi_line_buf[1] = mymalloc(SRAM12, jpeg_line_size * 4);
- p_jpeg_data_buf = mymalloc(SRAMIN, jpeg_buf_size); /* 为jpeg文件申请内存 */
- pname = mymalloc(SRAMIN, 30); /* 为带路径的文件名分配30个字节的内存 */
- while (pname == NULL || !p_dcmi_line_buf[0] || !p_dcmi_line_buf[1]
- || !p_jpeg_data_buf) /* 内存分配出错 */
- {
- text_show_string(30, 150, 240, 16, "内存分配失败!", 16, 0, RED);
- delay_ms(200);
- lcd_fill(30, 150, 240, 146, WHITE); /* 清除显示 */
- delay_ms(200);
- }
- while (ov2640_init()) /* 初始化OV2640 */
- {
- text_show_string(30, 150, 240, 16, "OV2640 错误!", 16, 0, RED);
- delay_ms(200);
- lcd_fill(30, 150, 239, 206, WHITE);
- delay_ms(200);
- }
- delay_ms(100);
- text_show_string(30, 170, 230, 16, "OV2640 正常", 16, 0, RED);
- ov2640_rgb565_mode(); /* RGB565模式 */
- dcmi_init(); /* DCMI配置 */
- dcmi_dma_init((uint32_t)&LCD->LCD_RAM, 0, 1,DMA_MDATAALIGN_HALFWORD,
- DMA_MINC_DISABLE); /* DCMI DMA配置,MCU屏,竖屏 */
- ov2640_outsize_set(lcddev.width, lcddev.height); /* 满屏缩放显示 */
- dcmi_start(); /* 启动传输 */
- ov2640_flash_intctrl(); /* 闪光灯控制 */
- lcd_clear(BLACK);
- while (1)
- {
- key = key_scan(0);
- if (key && (key != KEY2_PRES))
- {
- dcmi_stop(); /* 停止显示 */
- if (key == WKUP_PRES) /* 缩放处理 */
- {
- scale = !scale;
- if (scale == 0)
- {
- ov2640_image_win_set((1600 - lcddev.width) / 2,
- (1200 - lcddev.height) / 2, lcddev.width, lcddev.height);
- ov2640_outsize_set(lcddev.width, lcddev.height);
- sprintf((char *)msgbuf, "Full Size1:1");
- }
- else
- {
- ov2640_image_win_set(0,0,1600,1200); /* 全尺寸缩放 */
- ov2640_outsize_set(lcddev.width, lcddev.height);
- sprintf((char *)msgbuf, "Scale");
- }
-
- lcd_show_string(30, 50, 200, 16, 16, (char *)msgbuf, RED);
- delay_ms(800);
- }
- else if (sd_ok) /* SD卡正常才可以拍照 */
- {
- sw_sdcard_mode(); /* 切换为SD卡模式 */
- if (key == KEY0_PRES) /* BMP拍照 */
- {
- camera_new_pathname(pname, 0); /* 得到文件名 */
- res = bmp_encode(pname, 0, 0, lcddev.width, lcddev.height, 0);
- sw_ov2640_mode(); /* 切换为OV2640模式 */
- }
- else if (key == KEY1_PRES) /* JPG拍照 */
- {
- camera_new_pathname(pname, 1); /* 得到文件名 */
- res = ov2640_jpg_photo(pname);
- if (scale == 0)
- {
- ov2640_image_win_set((1280 - fac * lcddev.width) / 2,
- (800 - lcddev.height) / 2, lcddev.width, lcddev.height);
- }
- else
- {
- ov2640_image_win_set(0, 0, 1600, 1200); /* 全尺寸缩放 */
- }
- ov2640_outsize_set(lcddev.width, lcddev.height);
- }
- sw_ov2640_mode(); /* 切换为OV2640模式 */
-
- if (res) /* 拍照有误 */
- {
- text_show_string(30, 130, 240, 16, "写入文件错误!", 16, 0, RED);
- }
- else
- {
- text_show_string(30, 130, 240, 16, "拍照成功!", 16, 0, RED);
- text_show_string(30, 150, 240, 16, "保存为:", 16, 0, RED);
- text_show_string(30 + 56, 150, 240, 16, (char*)pname,16,0,RED);
- BEEP(1); /* 蜂鸣器短叫,提示拍照完成 */
- delay_ms(100);
- BEEP(0); /* 关闭蜂鸣器 */
- }
- delay_ms(1000); /* 等待1秒钟 */
- /* 这里先使能dcmi,然后立即关闭DCMI,后面再开启DCMI,可以防止RGB屏的侧移问题 */
- dcmi_start();
- dcmi_stop();
- }
- else /* 提示SD卡错误 */
- {
- text_show_string(30, 130, 240, 16, "SD卡错误!", 16, 0, RED);
- text_show_string(30, 150, 240, 16, "拍照功能不可用!", 16, 0, RED);
- }
- BEEP(0);
- if (key != WKUP_PRES) delay_ms(1800);
- dcmi_start(); /* 开始显示 */
- }
- delay_ms(10);
- i++;
- if (i == 20) /* DS0闪烁 */
- {
- i = 0;
- LED0_TOGGLE();
- }
- }
- }
复制代码该函数完成对各相关硬件的初始化,然后检测OV2640,初始化OV2640位RGB565模式,显示采集到的图像到LCD上面,实现对图像进行预览。进入主循环以后,按KEY0按键,可以实现BMP拍照(实际上就是截屏,通过bmp_encode函数实现);按KEY1按键,可实现JPEG拍照(1600*1200分辨率,通过ov2640_jpg_photo函数实现);按KEY_UP按键,可以实现图像缩放/不缩放预览。main函数实现了我们在54.2节所提到的功能。
至此照相机实验代码编写完成。最后,本实验可以通过USMART来设置OV2640的相关参数,将ov2640_read_reg、ov2640_write_reg函数添加到USMART管理,即可通过串口设置OV2640的参数,方便调试。
54.4 下载验证将程序下载到开发板后,可以看到LCD首先显示一些实验相关的信息,如图54.4.1所示: 显示了上图的信息后,自动进入监控界面。可以看到LED0不停的闪烁,提示程序已经在运行了。此外,LED1不停闪烁,提示进入DCMI中断回调服务函数,进行jpeg数据处理。此时,我们可以按下KEY0和KEY1,即可进行bmp/jpg拍照。拍照得到的照片效果如图54.4.2和图54.4.3所示: 按KEY_UP可以实现缩放/不缩放显示。最后,我们还可以通过USMART调用OV2640的相关控制函数,实现串口控制OV2640的在线参数修改,方便调试。 |