1)实验平台:正点原子MiniPro H750开发板 2)平台购买地址:https://detail.tmall.com/item.htm?id=677017430560 3)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-336836-1-1.html 4)对正点原子STM32感兴趣的同学可以加群讨论:879133275
第五十章 照相机实验
上一章,我们学习了如何使用STM32H750自带的硬件JPEG编解码器,实现对JPG/JPEG图片的硬解码,从而大大提高解码速度。本章我们将学习BMP&JPEG编码,结合前面的摄像头实验,实现一个简单的照相机。 本章分为如下几个小节: 50.1 BMP&JPEG编码简介 50.2 硬件设计 50.3 程序设计 50.4 下载验证
50.1 BMP&JPEG编码简介 我们要实现支持BMP图片格式的照片和JPEG图片格式的照片的照相机功能,这里简单介绍一下这两种图片格式的编码。这里我们使用ATK-OV5640-AF摄像头,来实现拍照。关于OV5640的相关知识点,请参考第四十三章。 50.1.1 BMP编码简介 前面的章节中,我们学习了各种图片格式的解码。本章,我们介绍最简单的图片编码方法:BMP图片编码。通过前面的了解,我们知道BMP文件是由文件头、位图信息头、颜色信息和图形数据等四部分组成。我们先来了解下这几个部分。 1、BMP文件头(14字节):BMP文件头数据结构含有BMP文件的类型、文件大小和位图起始位置等信息。
typedef __packed struct
{
uint16_t bfType ;
uint32_t bfSize ;
uint16_t bfReserved1 ;
uint16_t bfReserved2 ;
uint32_t bfOffBits ;
}BITMAPFILEHEADER ;
2、位图信息头(40字节):BMP位图信息头数据用于说明位图的尺寸等信息。 /* BMP信息头 */
typedef __packed struct
{
uint32_t biSize ;
long biWidth ;
long biHeight ;
uint16_t biPlanes ;
uint16_t biBitCount ;
uint32_t biCompression ;
uint32_t biSizeImage ;
long biXPelsPerMeter ;
long biYPelsPerMeter ;
uint32_t biClrUsed ;
uint32_t biClrImportant ;
}BITMAPINFOHEADER ;
3、颜色表:颜色表用于说明位图中的颜色,它有若干个表项,每一个表项是一个RGBQUAD类型的结构,定义一种颜色。 /* 彩色表 */
typedef __packed struct
{
uint8_t rgbBlue ;
uint8_t rgbGreen ;
uint8_t rgbRed ;
uint8_t rgbReserved ;
}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];
}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];
}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编码就介绍到这里。 50.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(480KB字节)来存储JPEG图像数据。而在本实验中,我们可以使用内存管理来申请内存,无需定义这么大的数组,使用上更加灵活。DCMI接口使用DMA直接传输JPEG数据,DMA接收到的JPEG数据放到内部SRAM。所以,我们本章将使用DMA的双缓冲机制来读取,DMA双缓冲读取JPEG数据框图如图50.1.2.1所示:
图50.1.2.1 DMA双缓冲读取JPEG数据原理框图 DMA接收来自OV5640的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拍照。 50.2 硬件设计
- 例程功能
1、首先是检测字库,然后检测SD卡根目录是否存在PHOTO文件夹,如果不存在则创建,如果创建失败,则报错(提示拍照功能不可用)。在找到SD卡的PHOTO文件夹后,开始初始化OV5640,如果初始化成功,则提示信息:KEY0:拍照(bmp格式),KEY1:拍照(jpg格式),WK_UP选择:1:1显示,即不缩放,图片不变形,但是显示区域小(液晶分辨率大小),或者缩放显示,即将1280*800的图像压缩到液晶分辨率尺寸显示,图片变形,但是显示了整个图片内容。可以通过串口1,借助USMART设置/读取OV5640的寄存器,方便大家调试。 2、LED0闪烁,提示程序运行。LED1用于指示帧中断。 - 硬件资源
1)RGB灯 RED :LED0 - PB4 GREEN :LED1 - PE6 2)串口1(PA9/PA10连接在板载USB转串口芯片CH340上面) 3)正点原子2.8/3.5/4.3/7/10寸TFTLCD模块(仅限MCU屏,16位8080并口驱动) 4)独立按键 :KEY0 - PA1、KEY1 - PA15、WK_UP - PA0 5)SD卡,通过SDMMC1(SDMMC_D0D4(PC8PC11), SDMMC_SCK(PC12),SDMMC_CMD(PD2))连接 6)norflash(QSPI FLASH芯片,连接在QSPI上) 7)硬件JPEG解码内核(STM32H750自带) 8)定时器6(用于打印摄像头帧率等信息) 9)ALIENTEK OV5640摄像头模块,连接关系为: OV5640模块 ----------- STM32开发板 OV_D0~D7 ------------ PC6/PC7/PC8/PC9/PC11/PD3/PB8/PB9 OV_SCL ------------ PB10 OV_SDA ------------ PB11 OV_VSYNC ------------ PB7 OV_HREF ------------ PA4 OV_RESET ------------ PA7 OV_PCLK ------------ PA6 OV_PWDN ------------ PC4 50.3 程序设计 50.3.1 程序流程图
图50.3.1.1 照相机实验程序流程图 50.3.2 程序解析
- PICTURE驱动代码
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。PICTURE驱动源码包括两个文件:bmp.c和bmp.h。 bmp.h头文件在50.1.1小节基本讲过,具体请看源码。下面来看到bmp.c文件里面的bmp编码函数:bmp_encode,该函数代码如下:
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;
BITMAPINFO hbmp;
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
databuf = (uint16_t *)piclib_mem_malloc(2048);
if (databuf == NULL)return PIC_MEM_ERR;
f_bmp = (FIL *)piclib_mem_malloc(sizeof(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);
my_mem_set((uint8_t *)&hbmp, 0, sizeof(hbmp));
hbmp.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
hbmp.bmiHeader.biWidth = width;
hbmp.bmiHeader.biHeight = height;
hbmp.bmiHeader.biPlanes = 1;
hbmp.bmiHeader.biBitCount = 16;
hbmp.bmiHeader.biCompression = BI_BITFIELDS;
hbmp.bmiHeader.biSizeImage = hbmp.bmiHeader.biHeight * hbmp.bmiHeader.biWidth * hbmp.bmiHeader.biBitCount / 8;
hbmp.bmfHeader.bfType = ((uint16_t)'M' << 8) + 'B';
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)
{
res = f_open(f_bmp, (const TCHAR *)filename, FA_WRITE | FA_CREATE_NEW);
}
if ((hbmp.bmiHeader.biWidth * 2) % 4)
{
bi4width = ((hbmp.bmiHeader.biWidth * 2) / 4 + 1) * 4;
}
else
{
bi4width = hbmp.bmiHeader.biWidth * 2;
}
if (res == FR_OK)
{
res = f_write(f_bmp, (uint8_t *)&hbmp, bmpheadsize, &bw);
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
piclib_mem_free(databuf);
piclib_mem_free(f_bmp);
#endif
return res;
}
该函数实现了对LCD屏幕的任意指定区域进行截屏保存,用到的方法就是50.1.1节我们所介绍的方法,该函数实现了将LCD任意指定区域的内容,保存个为16位BMP格式,存放在指定位置(由filename决定)。注意,代码中的BMP_USE_MALLOC是在bmp.h定义的一个宏,用于设置是否使用malloc,本章我们选择使用malloc。 2. main.c代码 main.c前面定义了一些变量和数组,具体如下: /* bmp拍照请求:0,无bmp拍照请求;1,有bmp拍照请求,需要在帧中断里面,关闭DCMI接口 */
volatile uint8_t g_bmp_request = 0;
uint8_t g_ovx_mode = 0;
uint16_t g_curline = 0;
uint16_t g_yoffset = 0;
#define jpeg_buf_size 440*1024
#define jpeg_line_size 2*1024
uint32_t *p_dcmi_line_buf[2];
uint32_t *p_jpeg_data_buf;
volatile uint32_t g_jpeg_data_len = 0;
volatile uint8_t g_jpeg_data_ok = 0;
在main.c里面,总共有7个函数,我们接下来分别介绍。首先是处理JPEG数据函数,其定义如下:
void jpeg_data_process(void)
{
uint16_t i;
uint16_t rlen;
uint32_t *pbuf;
g_curline = g_yoffset;
if (g_ovx_mode & 0X01)
{
if (g_jpeg_data_ok == 0)
{
__HAL_DMA_DISABLE(&g_dma_dcmi_handle);
rlen=jpeg_line_size-__HAL_DMA_GET_COUNTER(&g_dma_dcmi_handle);
if (g_jpeg_data_len > (jpeg_buf_size / 4 - rlen))
{
printf("g_jpeg_data_len1:%d\r\n", g_jpeg_data_len);
g_jpeg_data_ok = 1;
return;
}
pbuf = p_jpeg_data_buf + g_jpeg_data_len;
if (DMA1_Stream1->CR & (1 << 19))
{
for (i = 0; i < rlen; i++)
{
pbuf[i] = p_dcmi_line_buf[1][i];
}
}
else
{
for (i = 0; i < rlen; i++)
{
pbuf[i] = p_dcmi_line_buf[0][i];
}
}
g_jpeg_data_len += rlen;
g_jpeg_data_ok = 1;
}
if (g_jpeg_data_ok == 2)
{
__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;
}
}
else
{
if (g_bmp_request == 1)
{
dcmi_stop();
g_bmp_request = 0;
}
lcd_set_cursor(0, 0);
lcd_write_ram_prepare();
}
}
该函数用于处理JPEG数据的接收,在DCMI_IRQHandler函数(在dcmi.c里面)里面被调用,它与jpeg_dcmi_rx_callback函数和ov5640_jpg_photo函数共同控制JPEG的数据的采集。JPEG数据的接收,采用DMA双缓冲机制,缓冲数组为:p_dcmi_line_buf(u32类型,RGB屏接收RGB565数据时,也是用这个数组);数组大小为:jpeg_line_size,我们定义的是2*1024,即数组大小为8K字节(数组大小不能小于存储摄像头一行输出数据的大小);JPEG数据接收处理流程就是按图50.1.2.1所示流程来实现的。由DMA传输完成中断和DCMI帧中断,两个中断服务函数共同完成jpeg数据的采集。采集到的JPEG数据,全部存储在p_jpeg_data_buf数组里面,p_jpeg_data_buf数组采用内存管理,从内部SRAM申请440K内存作为JPEG数据的缓存。 接下来介绍的是JPEG数据接收回调函数,其定义如下:
void jpeg_dcmi_rx_callback(void)
{
uint16_t i;
volatile uint32_t *pbuf;
if (g_jpeg_data_len > (jpeg_buf_size / 4 - jpeg_line_size))
{
printf("g_jpeg_data_len:%d\r\n", g_jpeg_data_len);
return;
}
pbuf = p_jpeg_data_buf + g_jpeg_data_len;
if (DMA1_Stream1->CR & (1 << 19))
{
for (i = 0; i < jpeg_line_size; i++)
{
pbuf[i] = p_dcmi_line_buf[0][i];
}
g_jpeg_data_len += jpeg_line_size;
}
else
{
for (i = 0; i < jpeg_line_size; i++)
{
pbuf[i] = p_dcmi_line_buf[1][i];
}
g_jpeg_data_len += jpeg_line_size;
}
SCB_CleanInvalidateDCache();
}
这是jpeg数据接收的主要函数,通过判断DMA1_Stream1->CR寄存器,读取不同p_dcmi_line_buf里面的数据,存储到SRAM里面(p_jpeg_data_buf)。该函数由DMA的传输完成中断服务函数:DMA1_Stream1_IRQHandler调用。 接下来介绍的是切换为OV5640模式函数,其定义如下:
void sw_ov5640_mode(void)
{
GPIO_InitTypeDef gpio_init_struct;
ov5640_write_reg(0X3017, 0XFF);
ov5640_write_reg(0X3018, 0XFF);
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;
HAL_GPIO_Init(GPIOC, &gpio_init_struct);
}
因为SD卡和OV5640有几个IO共用,所以这几个IO需要分时复用。该函数用于切换GPIO8/9/11的复用功能为DCMI接口,并开启OV5640,这样摄像头模块,可以开始正常工作。 接下来介绍的是切换为SD卡模式函数,其定义如下:
void sw_sdcard_mode(void)
{
GPIO_InitTypeDef gpio_init_struct;
ov5640_write_reg(0X3017, 0X00);
ov5640_write_reg(0X3018, 0X00);
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_SDIO1;
HAL_GPIO_Init(GPIOC, &gpio_init_struct);
}
该数用于切换GPIO8/9/11的复用功能为SDMMC接口,并关闭OV5640,这样,SD卡可以开始正常工作。 接下来介绍的是文件名自增(避免覆盖)函数,其定义如下:
void camera_new_pathname(uint8_t *pname, uint8_t mode)
{
uint8_t res;
uint16_t index = 0;
FIL *ftemp;
ftemp = (FIL *)mymalloc(SRAMIN, sizeof(FIL));
if (ftemp == NULL) return;
while (index < 0XFFFF)
{
if (mode == 0)
{
sprintf((char *)pname, "0:PHOTO/PIC%05d.bmp", index);
}
else
{
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卡里面。 接下来介绍的是OV5640拍照jpg图片函数,其定义如下:
uint8_t ov5640_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));
if (f_jpg == NULL)return 0XFF;
g_ovx_mode = 1;
g_jpeg_data_ok = 0;
sw_ov5640_mode();
ov5640_jpeg_mode();
ov5640_outsize_set(4, 0, 1280, 800);
dcmi_rx_callback = jpeg_dcmi_rx_callback;
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_start();
while (g_jpeg_data_ok != 1);
g_jpeg_data_ok = 2;
while (g_jpeg_data_ok != 1);
dcmi_stop();
g_ovx_mode = 0;
sw_sdcard_mode();
printf("jpeg data size:%d\r\n", g_jpeg_data_len * 4);
pbuf = (uint8_t *)p_jpeg_data_buf;
jpglen = 0;
headok = 0;
for (i = 0; i < g_jpeg_data_len * 4; i++)
{
if ((pbuf[i] == 0XFF) && (pbuf[i + 1] == 0XD8))
{
jpgstart = i;
headok = 1;
}
if ((pbuf[i] == 0XFF) && (pbuf[i + 1] == 0XD9) && headok)
{
jpglen = i - jpgstart + 2;
break;
}
}
if (jpglen)
{
res = f_open(f_jpg, (const TCHAR *)pname, FA_WRITE | FA_CREATE_NEW);
if (res == 0)
{
pbuf += jpgstart;
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_ov5640_mode();
ov5640_rgb565_mode();
dcmi_dma_init((uint32_t)&LCD->LCD_RAM, 0, 1, DMA_MDATAALIGN_HALFWORD,
DMA_MINC_DISABLE);
myfree(SRAMIN, f_jpg);
return res;
}
该函数实现OV5640的JPEG图像采集,并保存图像到SD卡,完成JPEG拍照。该函数首先设置OV5640工作在JPEG模式,然后,设置输出分辨率为WXGA(1280*800)。然后,开始采集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;
float fac;
uint8_t *pname;
uint8_t key;
uint8_t i;
uint8_t sd_ok = 1;
uint8_t scale = 1;
uint8_t msgbuf[15];
uint16_t outputheight = 0;
sys_cache_enable();
HAL_Init();
sys_stm32_clock_init(240, 2, 2, 4);
delay_init(480);
usart_init(115200);
usmart_dev.init(240);
mpu_memory_protection();
led_init();
lcd_init();
key_init();
beep_init();
ov5640_init();
sw_sdcard_mode();
piclib_init();
my_mem_init(SRAMIN);
my_mem_init(SRAM12);
my_mem_init(SRAM4);
my_mem_init(SRAMDTCM);
my_mem_init(SRAMITCM);
exfuns_init();
f_mount(fs[0], "0:", 1);
f_mount(fs[1], "1:", 1);
while (fonts_init())
{
lcd_show_string(30, 50, 200, 16, 16, "Font Error!", RED);
delay_ms(200);
lcd_fill(30, 50, 240, 66, 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");
if (res != FR_EXIST && res != FR_OK)
{
res = f_mkdir("0:/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;
}
p_dcmi_line_buf[0] = mymalloc(SRAM12, jpeg_line_size * 4);
p_dcmi_line_buf[1] = mymalloc(SRAM12, jpeg_line_size * 4);
p_jpeg_data_buf = mymalloc(SRAMIN, jpeg_buf_size);
pname = mymalloc(SRAMIN, 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 (ov5640_init())
{
text_show_string(30, 150, 240, 16, "OV5640 错误!", 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, "OV5640 正常", 16, 0, RED);
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();
dcmi_init();
dcmi_dma_init((uint32_t)&LCD->LCD_RAM, 0, 1, DMA_MDATAALIGN_HALFWORD,
DMA_MINC_DISABLE);
if (lcddev.height >= 800)
{
g_yoffset = (lcddev.height - 800) / 2;
outputheight = 800;
ov5640_write_reg(0x3035, 0X51);
}
else
{
g_yoffset = 0;
outputheight = lcddev.height;
}
g_curline = g_yoffset;
ov5640_outsize_set(16, 4, lcddev.width, outputheight);
dcmi_start();
lcd_clear(BLACK);
while (1)
{
key = key_scan(0);
if (key)
{
if (key == KEY0_PRES)
{
delay_ms(300);
g_bmp_request = 1;
while (g_bmp_request);
}
else
{
dcmi_stop();
}
if (key == WKUP_PRES)
{
scale = !scale;
if (scale == 0)
{
fac = (float)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 (sd_ok)
{
sw_sdcard_mode();
if (key == KEY0_PRES)
{
camera_new_pathname(pname, 0);
res = bmp_encode(pname, 0, g_yoffset, lcddev.width,
outputheight, 0);
sw_ov5640_mode();
}
else if (key == KEY1_PRES)
{
camera_new_pathname(pname, 1);
res = ov5640_jpg_photo(pname);
if (scale == 0)
{
fac = (float)800 / outputheight;
ov5640_outsize_set((1280 - fac * lcddev.width) / 2, (800
- fac * outputheight) / 2, lcddev.width, outputheight);
}
else
{
ov5640_outsize_set(16, 4, lcddev.width, outputheight);
}
if (lcddev.height >= 800)ov5640_write_reg(0x3035, 0X51);
}
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);
dcmi_start();
dcmi_stop();
}
else
{
text_show_string(30, 130, 240, 16, "SD卡错误!", 16, 0, RED);
text_show_string(30, 150, 240, 16, "拍照功能不可用!", 16, 0, RED);
}
dcmi_start();
}
delay_ms(10);
i++;
if (i == 20)
{
i = 0;
LED0_TOGGLE();
}
}
}
该函数完成对各相关硬件的初始化,然后检测OV5640,初始化OV5640位RGB565模式,显示采集到的图像到LCD上面,实现对图像进行预览。进入主循环以后,按KEY0按键,可以实现BMP拍照(实际上就是截屏,通过bmp_encode函数实现);按KEY1按键,可实现JPEG拍照(1280*800分辨率,通过ov5640_jpg_photo函数实现);按KEY_UP按键,可以实现图像缩放/不缩放预览。main函数实现了我们在50.2节所提到的功能。 至此照相机实验代码编写完成。最后,本实验可以通过USMART来设置OV5640的相关参数,将ov5640_contrast、ov5640_color_saturation和ov5640_light_mode等函数添加到USMART管理,即可通过串口设置OV5640的参数,方便调试。 50.4 下载验证 将程序下载到开发板后,可以看到LCD首先显示一些实验相关的信息,如图50.4.1所示:
图50.4.1显示实验相关信息 显示了上图的信息后,自动进入监控界面。可以看到LED0不停的闪烁,提示程序已经在运行了。此外,LED1不停闪烁,提示进入DCMI中断回调服务函数,进行jpeg数据处理。此时,我们可以按下KEY0和KEY1,即可进行bmp/jpg拍照。拍照得到的照片效果如图50.4.2和图50.4.3所示:
图50.4.2 拍照样图(bmp拍照样图)
图50.4.3 拍照样图(jpg拍照样图) 按KEY_UP可以实现缩放/不缩放显示。最后,我们还可以通过USMART调用OV5640的相关控制函数,实现串口控制OV5640的在线参数修改,方便调试。
|