《STM32H7R7开发指南 V1.1 》第五十八章 照相机实验

第五十八章 照相机实验


       上一章,我们学习了图片解码,本章我们将学习BMP&JPEG编码,结合前面的摄像头实验,实现一个简单的照相机。

       本章分为如下几个小节:

       58.1 BMP&JPEG编码简介 

       58.2 硬件设计

       58.3 程序设计

       58.4 下载验证


        58.1 BMP&JPEG编码简介

       我们要实现支持BMP图片格式的照片和JPEG图片格式的照片的照相机功能,这里简单介绍一下这两种图片格式的编码。这里我们使用ATK-OV5640-AF摄像头,来实现拍照。关于OV5640的相关知识点,请参考第五十章。


       58.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编码就介绍到这里。


       58.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拍照,并不需要自己压缩图像,因为我们使用的正点原子 OV5640摄像头模块,直接就可以输出压缩后的JPEG数据,我们完全不需要理会压缩过程,所以本实验我们实现JPEG拍照的关键,在于准确接收OV5640摄像头模块发送过来的编码数据,然后将这些数据保存为.jpg文件,就可以实现JPEG拍照了。

       在第五十章的摄像头实验中,我们定义了一个很大的数组g_jpeg_data_buf(1MB字节)来存储JPEG图像数据。而在本实验中,我们可以使用内存管理来申请内存,无需定义这么大的数组,使用上更加灵活。DCMIPP接口使用HPDMA直接传输JPEG数据,HPDMA接收到的JPEG数据放到内部SRAM。所以,我们本章将使用HPDMA的双缓冲机制来读取,HPDMA双缓冲读取JPEG数据框图如图58.1.2.1所示:


图58.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拍照。


        58.2 硬件设计


       1. 例程功能


       1、首先是检测字库,然后检测SD卡根目录是否存在PHOTO文件夹,如果不存在则创建,如果创建失败,则报错(提示拍照功能不可用)。在找到SD卡的PHOTO文件夹后,开始初始化OV5640,在初始化成功之后,就一直在屏幕显示OV5640拍到的内容。当按下KEY0,可以拍bmp图片照片(分辨率为:LCD辨率),按下KEY1,可以拍JPEG图片照片。拍照保存成功之后,蜂鸣器会发出“滴”的一声,提示拍照成功。按下KEY2,执行一次自动对焦;按下KEY_UP,可以切换尺寸大小。


       2、LED0闪烁,提示程序运行。LED1用于指示帧中断。


       2. 硬件资源


       1)LED灯  

              LED0 :LED0 – PD14

              LED1 :LED1 – PC0


       2)串口1(PB14/PB15连接在板载USB转串口芯片CH340上面)


       3)正点原子2.8/3.5/4.3/7/10寸TFTLCD模块(包括MCU屏和RGB屏,都支持)


       4)独立按键 :KEY0 – PE9、KEY1 – PE8、KEY2 – PE7、WK_UP – PC13


       5)SD卡,通过SDMMC(SDMMC_D0~D4(PC8~PC11),

       SDMMC_SCK(PC12),SDMMC_CMD(PD2))连接


       6)norflash


       7)DCMIPP接口(用于驱动OV5640摄像头模块)


       8)正点原子OV5640摄像头模块,连接关系为:

              OV5640模块 -----------   STM32开发板

              OV_D0~D7  ------------   PC6/PC7/PE0/PE1/PE4/PB6/PB8/PB9

              OV_SCL    ------------     PM3

              OV_SDA    ------------    PM2

              OV_VSYNC  ------------  PB7

              OV_HREF   ------------   PG3

              OV_RESET  ------------   PE2

              OV_PCLK   ------------   PA6

              OV_XCLK   -----------    PD13

              OV_PWDN  ------------   PF5


        58.3 程序设计 


       58.3.1 程序解析


       1. PICTURE驱动代码

       这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。PICTURE驱动源码包括两个文件:bmp.c和bmp.h。 

       bmp.h头文件在58.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格式标志 */
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, (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屏幕的任意指定区域进行截屏保存,用到的方法就是58.1.1节我们所介绍的方法,该函数实现了将LCD任意指定区域的内容,保存为16位BMP格式,存放在指定位置(由filename决定)。注意,代码中的BMP_USE_MALLOC是在bmp.h定义的一个宏,用于设置是否使用malloc,本章我们选择使用malloc。


       2. main.c代码

       main.c前面定义了一些变量和数组,具体如下:

static uint8_t g_bmp_request = 0;                /* BMP拍照请求标志位 */
static uint8_t g_ov_mode;                        /* OV5640输出模式标志 */
static uint8_t g_jpeg_data_ok;                   /* JPEG帧数据采集标志 */
static uint32_t g_jpeg_data_len = 0;             /* JPEG帧数据缓存中有效数据长度 */

       在main.c里面,总共有7个函数,我们接下来分别介绍。首先是处理JPEG数据函数,其定义如下:

/**
 * @brief       处理JPEG数据
 * @ntoe        在DCMIPP_IRQHandler中断服务函数里面被调用
 *              当采集完一帧JPEG数据后,调用此函数,切换JPEG BUF.开始下一帧采集.
 *
 * @param       无
 * @retval      无
 */
void jpeg_data_process(void)
{
    if (g_ov_mode != 0)               /* 只有在JPEG格式下,才需要做处理. */
    {
        if (g_jpeg_data_ok == 0)      /* jpeg数据还未采集完? */
        { 
            dcmipp_stop();            /* 关闭数据传输 */
            HAL_DCMIPP_PIPE_GetDataCounter(&g_dcmipp_handle, DCMIPP_PIPE0, 
(uint32_t*)&g_jpeg_data_len);
            g_jpeg_data_ok = 1;       /* 标记JPEG数据采集完成,等待其他函数处理 */                                                               
        }
        /* 采集的JPEG数据已被处理 */
        if (g_jpeg_data_ok == 2)
        {
            /* 重新开始采集JPEG数据 */
            g_jpeg_data_ok = 0;
            g_jpeg_data_len = 0;
            dcmipp_start();
        }
    }
    else
    {
        /* 有BMP拍照请求,则停止DCMIPP传输 */
        if (g_bmp_request != 0)
        {
            g_bmp_request = 0;
            dcmipp_stop();
        }
        
//        /* MCU屏,则重置光标位置 */
//        if (lcdltdc.pwidth == 0)
//        {
            lcd_set_cursor(0, 0);
            lcd_write_ram_prepare();
//        }
//        
    }
}

       该函数用于处理JPEG数据的接收,在DCMIPP_IRQHandler函数(在dcmipp.c里面)里面被调用,它与HAL_DCMIPP_PIPE_FrameEventCallback函数控制JPEG的数据的采集。

       接下来介绍的是文件名自增(避免覆盖)函数,其定义如下:

/**
 * @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卡里面。

       接下来介绍的是拍照jpeg图片函数,其定义如下:

/**
 * @bref    保存JPEG图片
 * @param   fname: 文件名
 * @retval  保存结果
 * @arg     0: 成功
 * @arg     其他: 失败
 */
static uint8_t camera_jpeg_photo(char *fname)
{
    FIL *f = NULL;
    uint8_t *buf;
    uint32_t i;
    uint32_t jpeg_header = UINT32_MAX;
    uint32_t jpeg_length = 0;
    uint8_t res;
    uint32_t bw;
    
    /* 申请内存 */
    f = (FIL *)mymalloc(SRAMIN, sizeof(FIL));
    if (f == NULL)
    {
        return 0xFF;
    }
    
    /* 配置OV5640输出JPEG */
    g_ov_mode = 1;
    ov5640_jpeg_mode();
    ov5640_outsize_set(16, 4, 2592, 1944);
    
    /* 采集JPEG数据 */
    g_jpeg_data_ok = 0;
    g_jpeg_data_len = 0; 
    dcmipp_start();
    while (g_jpeg_data_ok != 1);
    g_jpeg_data_ok = 2;
    dcmipp_start();
    while (g_jpeg_data_ok != 1);
    /* 处理JPEG数据 */
    dcmipp_stop();
    buf = (uint8_t *)camera_date_buf;
    for (i=1; i<(g_jpeg_data_len * sizeof(uint32_t)); i++)
    {
        /* 找JPEG头部(0xFF 0xD8) */
        if ((buf[i - 1] == 0xFF) && (buf[i] == 0xD8))
        {
            jpeg_header = i - 1;
        }
        
        /* 找JPEG头部后,再找JPEG尾部(0xFF 0xD9) */
        if ((jpeg_header != UINT32_MAX) && ((buf[i - 1] == 0xFF) && (buf[i] == 
0xD9)))
        {
            jpeg_length = (i - 1) - jpeg_header + 2;
            break;
        }
    }
    
    printf("JPEG Data Size: %d\r\n", jpeg_length);
    
    /* 将JPEG数据写入文件 */
    if (jpeg_length != 0)
    {
        res = f_open(f, fname, FA_WRITE | FA_CREATE_NEW);
        if (res == FR_OK)
        {
            res = f_write(f, &buf[jpeg_header], jpeg_length, &bw);
            f_close(f);
        }
    }
    else
    {
        res = 0xFD;
    }
    
    /* 配置OV5640输出RGB565 */
    g_ov_mode = 0;
    ov5640_rgb565_mode();
    
    /* 配置DCMIPP */
    dcmipp_start();
    g_jpeg_data_ok = 0;
    g_jpeg_data_len = 0;
    myfree(SRAMIN, f);
    
    return res;
}

       该函数实现OV5640的JPEG图像采集,并保存图像到SD卡,完成JPEG拍照。该函数首先设置OV5640工作在JPEG模式,然后,设置输出分辨率为QSXGA(2592*1944)。然后,开始采集JPEG数据,将第二帧JPEG数据,保留下来,并写入SD卡里面,完成一次JPEG拍照。这里,我们丢弃第一帧JPEG数据,是防止采集到的图像数据不完整,导致图片错误。

       另外,在保存jpeg图片的时候,我们将0XFF,0XD8和0XFF,0XD9之外的数据,进行了剔除,只留下0XFF,0XD8~0XFF,0XD9之间的数据,保证图片文件最小,且无其他乱的数据。

       最后介绍的是main函数,其定义如下:

int main(void)
{
    uint8_t res;
    uint8_t sd_ok = 1;
    char *fname = NULL;
    uint16_t output_height;
    uint8_t t = 0;
    uint8_t key;
    uint8_t scale;
    float fac;
    
    sys_mpu_config();                   /* 配置MPU */
    sys_cache_enable();                 /* 使能Cache */
    HAL_Init();                         /* 初始化HAL库 */
    sys_stm32_clock_init(300, 6, 2);    /* 配置时钟,600MHz */
    delay_init(600);                    /* 初始化延时 */
    usart_init(115200);                 /* 初始化串口 */
    led_init();                         /* 初始化LED */
    beep_init();                        /* 初始化蜂鸣器 */
    key_init();                         /* 初始化按键 */
    hyperram_init();                    /* 初始化HyperRAM */
    lcd_init();                         /* 初始化LCD */
    my_mem_init(SRAMIN);                /* 初始化AXI-SRAM1~4内存池 */
    my_mem_init(SRAMEX);                /* 初始化XSPI2 HyperRAM内存池 */
    my_mem_init(SRAM12);                /* 初始化AHB-SRAM1~2内存池 */
    my_mem_init(SRAMDTCM);              /* 初始化DTCM内存池 */
    my_mem_init(SRAMITCM);              /* 初始化ITCM内存池 */
    exfuns_init();                      /* 为exfuns申请内存 */
    f_mount(fs[0], "0:", 1);            /* 挂载SD卡 */
    f_mount(fs[1], "1:", 1);            /* 挂载NOR Flash */
    f_mount(fs[2], "2:", 1);            /* 挂载NAND 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, "KEY2: 自动对焦", 16, 0, RED);
    text_show_string(30, 150, 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, 170, 200, 16, "SD卡错误!", 16, 0, RED);
        text_show_string(30, 190, 200, 16, "拍照功能将不可用!", 16, 0, RED);
        delay_ms(500);
    }
    
    /* 申请内存 */
    fname = (char *)mymalloc(SRAMIN, 30);
    camera_date_buf = mymalloc(SRAMEX, 3 * 1024 * 1024 / 4);
    while ((camera_date_buf == NULL) || (fname == NULL))
    {
        text_show_string(30, 210, 200, 16, "内存分配失败!", 16, 0, RED);
        delay_ms(500);
        lcd_fill(30, 210, 30 + 200, 210 + 16, WHITE);
        delay_ms(500);
    }
    
    /* 初始化OV5640 */
    while (ov5640_init() != 0)
    {
        text_show_string(30, 210, 200, 16, "OV5640错误!", 16, 0, RED);
        delay_ms(500);
        lcd_fill(30, 210, 30 + 200, 210 + 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)
    {
        output_height = 800;
        ov5640_write_reg(0x3035, 0x51);
    }
    else if (lcddev.height == 1280)
    {
        output_height = 600;
        ov5640_write_reg(0x3035, 0x51);
    }
    else 
    {
        output_height = lcddev.height;
    }
    scale = 1;
    fac = (float)800.f / output_height;
    ov5640_outsize_set(4, 0, lcddev.width, output_height);
    
    /* 配置DCMIPP */
    dcmipp_init();
    lcd_clear(BLACK);
    dcmipp_start();
    
    while (1)
    {
        lcd_color_fill(0, 0, lcddev.width - 1, lcddev.height - 1, 
(uint16_t*)camera_date_buf);
        key = key_scan(0);
        if ((key == KEY0_PRES) || (key == KEY1_PRES))
        {
            if (sd_ok == 0)
            {
                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拍照 */
                    camera_new_pathname(fname, 0);
                    res = bmp_encode((uint8_t *)fname, 0, 0, lcddev.width, 
output_height, 0);
                }
                else if (key == KEY1_PRES)
                {
                    /* JPEG拍照 */
                    dcmipp_stop();
                    camera_new_pathname(fname, 1);
                    res = camera_jpeg_photo(fname);
                    
                    /* 重新配置OV5640 */
                    if ((lcddev.height == 1024) || (lcddev.height == 1280))
                    {
                        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);
                }
                delay_ms(1000);
                dcmipp_start();
            }
        }
        else if (key == KEY2_PRES)
        {
            /* OV5640单次自动对焦 */
            ov5640_focus_single();
        }
        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拍照(2592*1944分辨率,通过ov5640_jpg_photo函数实现);按KEY_UP按键,可以实现图像缩放/不缩放预览;按KEY2按键,可以进行单次对焦。main函数实现了我们在58.2节所提到的功能。 


        58.4 下载验证

       将程序下载到开发板后,可以看到LCD首先显示一些实验相关的信息,如图58.4.1所示:


图58.4.1显示实验相关信息


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


图58.4.2 拍照样图(bmp拍照样图)


图58.4.3 拍照样图(jpg拍照样图)


       按KEY_UP可以实现缩放/不缩放显示。按KEY2可以实现单次自动对焦。

       本实验的讲解就到这里,大家可以动手验证一下。


请使用浏览器的分享功能分享到微信等