搜索
bottom↓
回复: 0

《MiniPro STM32H750 开发指南》第五十章 照相机实验

[复制链接]

出0入234汤圆

发表于 2022-9-27 10:01:10 | 显示全部楼层 |阅读模式
本帖最后由 正点原子 于 2022-9-27 10:01 编辑

1)实验平台:正点原子MiniPro STM32H750开发板
2)平台购买链接:https://detail.tmall.com/item.htm?id=677017430560
3)全套实验源码+手册+视频下载地址:http://www.openedv.com/docs/boar
4)MiniPro STM32H750技术交流QQ群:756580169 lQLPJxaFi2zaB4UWWrDAMgIsFEW2pwLb3abnwDMA_90_22.png

lQDPJxaFi2nfFizMjM0CbLCPlxn_FVheIQLb3aGrwFQA_620_140.jpg

lQLPJxaFi2nfFhLMkM0BXrDNvOUyeU_FPgLb3aGvQNIA_350_144.png

第五十章 照相机实验


上一章,我们学习了如何使用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文件的类型、文件大小和位图起始位置等信息。  
/* 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编码就介绍到这里。
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所示:
第五十章 照相机实验5523.png
图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. 例程功能
1、首先是检测字库,然后检测SD卡根目录是否存在PHOTO文件夹,如果不存在则创建,如果创建失败,则报错(提示拍照功能不可用)。在找到SD卡的PHOTO文件夹后,开始初始化OV5640,如果初始化成功,则提示信息:KEY0:拍照(bmp格式),KEY1:拍照(jpg格式),WK_UP选择:1:1显示,即不缩放,图片不变形,但是显示区域小(液晶分辨率大小),或者缩放显示,即将1280*800的图像压缩到液晶分辨率尺寸显示,图片变形,但是显示了整个图片内容。可以通过串口1,借助USMART设置/读取OV5640的寄存器,方便大家调试。
2、LED0闪烁,提示程序运行。LED1用于指示帧中断。
2. 硬件资源
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_D0~D4(PC8~PC11),
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 程序流程图

E9ADA328-ADE5-4f9f-969A-1F1FA80E7433.png
图50.3.1.1 照相机实验程序流程图

50.3.2 程序解析
1. PICTURE驱动代码
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。PICTURE驱动源码包括两个文件:bmp.c和bmp.h。
bmp.h头文件在50.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屏幕的任意指定区域进行截屏保存,用到的方法就是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;                      /* bit0:0,RGB565模式;1,JPEG模式 */
uint16_t g_curline = 0;                      /* 摄像头输出数据,当前行编号 */
uint16_t g_yoffset = 0;                      /* y方向的偏移量 */

#define jpeg_buf_size   440*1024    /* 定义JPEG数据缓存jpeg_buf的大小(440K字节) */
#define jpeg_line_size  2*1024      /* 定义DMA接收数据时,一行数据的最大值 */

uint32_t *p_dcmi_line_buf[2];              /* RGB屏时,摄像头采用一行一行读取,定义行缓存 */
uint32_t *p_jpeg_data_buf;                 /* JPEG数据缓存buf */

volatile uint32_t g_jpeg_data_len = 0; /* buf中的JPEG有效数据长度 */

volatile uint8_t g_jpeg_data_ok = 0; /* JPEG数据采集完成标志
                                                         * 0,数据没有采集完;
                                                             * 1,数据采集完了,但是还没处理;
                                                         * 2,数据已经处理完成了,可以开始下一帧接收
                                                        */
在main.c里面,总共有7个函数,我们接下来分别介绍。首先是处理JPEG数据函数,其定义如下:
/**
* @brief       处理JPEG数据
*   @note      当采集完一帧JPEG数据后,调用此函数,切换JPEG BUF.开始下一帧采集
* @param       无
* @retval      无
*/
void jpeg_data_process(void)
{
    uint16_t i;
    uint16_t rlen;            /* 剩余数据长度 */
    uint32_t *pbuf;
    g_curline = g_yoffset;          /* 行数复位 */
    if (g_ovx_mode & 0X01)          /* 只有在JPEG格式下,才需要做处理 */
    {
        if (g_jpeg_data_ok == 0)    /* jpeg数据还未采集完 */
        {
            __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))      
            {
/* 打印当前长度(uint32_t)*/
                printf("g_jpeg_data_len1:%d\r\n", g_jpeg_data_len);
                g_jpeg_data_ok = 1; /* 标记JPEG数据采集完按成,等待其他函数处理 */
                return;
            }
            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;        /* 数据重新开始 */
        }
    }
    else
    {
        if (g_bmp_request == 1)         /* 有bmp拍照请求,关闭DCMI */
        {
            dcmi_stop();                  /* 停止DCMI */
            g_bmp_request = 0;           /* 标记请求处理完成 */
        }
        lcd_set_cursor(0, 0);
        lcd_write_ram_prepare();        /* 开始写入GRAM */
    }
}
该函数用于处理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数据接收回调函数,其定义如下:
/**
* @brief       JPEG数据接收回调函数
* @param       无
* @retval      无
*/
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))
{
/* 打印当前长度(uint32_t) */
        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))                   /* 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;            /* 偏移 */
    }
    SCB_CleanInvalidateDCache();                       /* 清除无效化DCache */
}
这是jpeg数据接收的主要函数,通过判断DMA1_Stream1->CR寄存器,读取不同p_dcmi_line_buf里面的数据,存储到SRAM里面(p_jpeg_data_buf)。该函数由DMA的传输完成中断服务函数:DMA1_Stream1_IRQHandler调用。
接下来介绍的是切换为OV5640模式函数,其定义如下:
/**
* @brief       切换为OV5640模式
*   @note      切换PC8/PC9/PC11为DCMI复用功能(AF13)
* @param       无
* @retval      无
*/
void sw_ov5640_mode(void)
{
    GPIO_InitTypeDef gpio_init_struct;
   
    ov5640_write_reg(0X3017, 0XFF); /* 开启OV5650输出(可以正常显示) */
    ov5640_write_reg(0X3018, 0XFF);
   
    /* 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卡和OV5640有几个IO共用,所以这几个IO需要分时复用。该函数用于切换GPIO8/9/11的复用功能为DCMI接口,并开启OV5640,这样摄像头模块,可以开始正常工作。
接下来介绍的是切换为SD卡模式函数,其定义如下:
/**
* @brief       切换为SD卡模式
*   @note      切换PC8/PC9/PC11为SDMMC复用功能(AF12)
* @param       无
* @retval      无
*/
void sw_sdcard_mode(void)
{
    GPIO_InitTypeDef gpio_init_struct;
   
    ov5640_write_reg(0X3017, 0X00); /* 关闭OV5640全部输出(不影响SD卡通信) */
    ov5640_write_reg(0X3018, 0X00);
   
    /* 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_SDIO1;               /* 复用为SDIO */
    HAL_GPIO_Init(GPIOC, &gpio_init_struct);             /* 初始化PC8,9, 11引脚 */
}
该数用于切换GPIO8/9/11的复用功能为SDMMC接口,并关闭OV5640,这样,SD卡可以开始正常工作。
接下来介绍的是文件名自增(避免覆盖)函数,其定义如下:
/**
* @brief       文件名自增(避免覆盖)
*   @note      bmp组合成: 形如 "0:PHOTO/PIC13141.bmp" 的文件名
*               jpg组合成: 形如 "0:PHOTO/PIC13141.jpg" 的文件名
* @param       pname : 有效的文件名
* @param       mode  : 0, 创建.bmp文件;  1, 创建.jpg文件;
* @retval      无
*/
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));   /* 开辟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卡里面。
接下来介绍的是OV5640拍照jpg图片函数,其定义如下:
/**
* @brief       OV5640拍照jpg图片
* @param       pname : 要创建的jpg文件名(含路径)
* @retval      0, 成功; 其他,错误代码;
*/
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));   /* 开辟FIL字节的内存区域 */
    if (f_jpg == NULL)return 0XFF;  /* 内存申请失败 */

    g_ovx_mode = 1;
    g_jpeg_data_ok = 0;
    sw_ov5640_mode();                                                               /* 切换为OV5640模式 */
    ov5640_jpeg_mode();                                                             /* JPEG模式 */
    ov5640_outsize_set(4, 0, 1280, 800);                 /* 设置输出尺寸(WXGA) */
    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配置 */
    dcmi_start();                              /* 启动传输 */
    while (g_jpeg_data_ok != 1);            /* 等待第一帧图片采集完 */
    g_jpeg_data_ok = 2;                       /* 忽略本帧图片,启动下一帧采集 */
    while (g_jpeg_data_ok != 1);           /* 等待第二帧图片采集完,第二帧,才保存到SD卡去 */

    dcmi_stop();         /* 停止DMA搬运 */
    g_ovx_mode = 0;
    sw_sdcard_mode();   /* 切换为SD卡模式 */
    printf("jpeg data size:%d\r\n", g_jpeg_data_len * 4);/*串口打印JPEG文件大小 */
    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_ov5640_mode();        /* 切换为OV5640模式 */
    ov5640_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;
}
该函数实现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;           /* 0,sd卡不正常;1,SD卡正常 */
    uint8_t scale = 1;           /* 默认是全尺寸缩放 */
    uint8_t msgbuf[15];                 /* 消息缓存区 */
    uint16_t outputheight = 0;

    sys_cache_enable();                           /* 打开L1-Cache */
    HAL_Init();                                     /* 初始化HAL库 */
    sys_stm32_clock_init(240, 2, 2, 4);        /* 设置时钟, 480Mhz */
    delay_init(480);                               /* 延时初始化 */
    usart_init(115200);                           /* 串口初始化为115200 */
    usmart_dev.init(240);                         /* 初始化USMART */
    mpu_memory_protection();                     /* 保护相关存储区域 */
    led_init();                                     /* 初始化LED */
    lcd_init();                                     /* 初始化LCD */
    key_init();                                     /* 初始化按键 */
    beep_init();                                    /* 初始化蜂鸣器 */
    ov5640_init();                                 /* 初始化OV5640 */
    sw_sdcard_mode();                              /* 首先切换为OV5640模式 */
    piclib_init();                                 /* 初始化画图 */
    my_mem_init(SRAMIN);                          /* 初始化内部内存池(AXI) */
    my_mem_init(SRAM12);                          /* 初始化SRAM12内存池(SRAM1+SRAM2) */
    my_mem_init(SRAM4);                           /* 初始化SRAM4内存池(SRAM4) */
    my_mem_init(SRAMDTCM);                        /* 初始化DTCM内存池(DTCM) */
    my_mem_init(SRAMITCM);                        /* 初始化ITCM内存池(ITCM) */
    exfuns_init();                                 /* 为fatfs相关变量申请内存 */
    f_mount(fs[0], "0:", 1);                     /* 挂载SD卡 */
    f_mount(fs[1], "1:", 1);                     /* 挂载FLASH */
//    lcd_display_dir(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");  /* 创建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 (ov5640_init())   /* 初始化OV5640 */
    {
        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();                /* 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配置 */
dcmi_dma_init((uint32_t)&LCD->LCD_RAM, 0, 1, DMA_MDATAALIGN_HALFWORD,
DMA_MINC_DISABLE);             /* DCMI DMA配置,MCU屏,竖屏 */
    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)
        {
/* 如果是BMP拍照,则等待1秒钟,去抖动,以获得稳定的bmp照片 */
            if (key == KEY0_PRES)
            {
                delay_ms(300);
                g_bmp_request = 1;              /* 请求关闭DCMI */
                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)                     /* SD卡正常才可以拍照 */
            {
                sw_sdcard_mode();               /* 切换为SD卡模式 */
                if (key == KEY0_PRES)          /* BMP拍照 */
                {
                    camera_new_pathname(pname, 0);          /* 得到文件名 */
                    res = bmp_encode(pname, 0, g_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 = (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); /* 等待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);
            }
            dcmi_start();   /* 开始显示 */
        }
        delay_ms(10);
        i++;
        if (i == 20)    /* DS0闪烁 */
        {
            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所示:
第五十章 照相机实验30103.png
图50.4.1显示实验相关信息

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

第五十章 照相机实验30291.png
图50.4.3 拍照样图(jpg拍照样图)

按KEY_UP可以实现缩放/不缩放显示。最后,我们还可以通过USMART调用OV5640的相关控制函数,实现串口控制OV5640的在线参数修改,方便调试。


阿莫论坛20周年了!感谢大家的支持与爱护!!

月入3000的是反美的。收入3万是亲美的。收入30万是移民美国的。收入300万是取得绿卡后回国,教唆那些3000来反美的!
回帖提示: 反政府言论将被立即封锁ID 在按“提交”前,请自问一下:我这样表达会给举报吗,会给自己惹麻烦吗? 另外:尽量不要使用Mark、顶等没有意义的回复。不得大量使用大字体和彩色字。【本论坛不允许直接上传手机拍摄图片,浪费大家下载带宽和论坛服务器空间,请压缩后(图片小于1兆)才上传。压缩方法可以在微信里面发给自己(不要勾选“原图),然后下载,就能得到压缩后的图片】。另外,手机版只能上传图片,要上传附件需要切换到电脑版(不需要使用电脑,手机上切换到电脑版就行,页面底部)。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

手机版|Archiver|amobbs.com 阿莫电子技术论坛 ( 粤ICP备2022115958号, 版权所有:东莞阿莫电子贸易商行 创办于2004年 (公安交互式论坛备案:44190002001997 ) )

GMT+8, 2024-4-24 18:24

© Since 2004 www.amobbs.com, 原www.ourdev.cn, 原www.ouravr.com

快速回复 返回顶部 返回列表