目录
吐槽
(开局先让我吐槽一下,大概三千来字,因为我真的很想吐槽,想直接看干货的可以跳过)
最近有用到摄像头,我们上某宝一搜摄像头模块,买的最多的就是OV系列的,比如说OV7670、OV2640、OV5640之类的。
价格上OV7670是差不多五块左右,OV2640是二十多块(不带驱动板,但是有基础的LDO,如果是带排线的纯摄像头的话只要十多块),OV5640就差不多要四五十了(不带驱动板,如果是纯摄像头更便宜一点)
本着能省一点是一点的原则,我用了OV7670,实际上我在去年就买了OV7670和OV2640这两个模块,只是一直懒得去玩,最近要做毕业设计了用到摄像头所以拿出来耍了耍,结果都是坑啊。
网上资料太多零碎,而且很多抄过来抄过去的,一堆什么野火啊原子啊乱七八糟的。
同一个时序,一堆人说是高电平有效,另一堆人说低电平有效,百家争鸣重出江湖了好叭。
更离谱的是商家提供的所谓能直接用的代码都是有问题的,有些商家贴心的提供了多份代码,然而我确信没有一份是自己写的,代码风格不一样不说,你连别家的版权信息都留上面了,还不止一家!
而且有些代码我强烈怀疑都是AI生成的,你问我为什么?
因为***配置寄存器的代码配置一大堆没用的寄存器,人家寄存器所有位都是保留位你去配置它干什么?为了后续OV7670更新的时候能够适配代码?人家OV7670早停产了好吧。
一些寄存器还给它写入了默认值???这就好像是我跑到你家里把你裤子扒了脱下你的内裤再给你穿回去然后把你裤子提起来再跳支舞接着离开你的家留下一脸懵逼的你。
多此一举。
还有一些寄存器在初始化的时候反反复复配置了好几次,我直接??????最后生效的是最后一次配置的,那你之前配置干嘛???我寻思OV7670关于配置寄存器也没有时序的要求啊???(除了一个控制复位的寄存器,给它赋值让复位之后还需要再配置一次)
商家给的中文手册有明显错误,我就随便截一个出来。
不是哥们,一些机翻的生硬问题咱就不说了,你这个配置直接给我翻反了是不是太过分了!给我气羊尾了都!
后面我一测试发现OV7670和OV2640的ID我都读不出来,更不用说拍照获取图像数据了,但是我的代码和网上人家介绍的基本一样所以我断定是硬件模块坏了(肯定不是我代码敲错了——来自程序员的骄傲)
于是我又重新买了OV7670和OV2640,甚至我还把带FIFO的OV7670的也买了(为了以防万一,关于FIFO是什么东西以及上面的一堆东西是什么,等我吐槽完再慢慢说,毕竟这是全网最详细的OV7670的文章嘛)
等了几天到货了,我发现ID还是读不出来,但是我代码比对过了,人家都可以读出来ID我却读不出来,后面我才发现,哥们,你讲的代码是没错,可是你**的没跟我说读个ID还要给它提供时钟信号啊!?我还以为只有读图像数据的时候才要呢!?
而且那么多文章,大多数都没提到这个问题,上来就是讲SCCB的时序,然后读一下寄存器,丝毫不提给OV7670的时钟信号的问题。
OK,或许这是很基础的问题,常识性的问题,可是我对于这方面完全是小白啊,就不能稍微提一下下吗?
后面我知道了人家不是故意不提的,而是他们可能自己都不知道需要给时钟信号,因为他们用的是带FIFO的OV7670,硬件自带晶振提供时钟信号。
算了,吃一堑长一智,堑吃多了成弱智。
OV7670我一共买了两次,因为第一次那个我以为是坏的(实际上没有,只是因为我没给它提供时钟信号所以没法正常工作),所以第二次我买的别家的。
而两家提供的资料是一模一样的,没错!一模一样!各种乱七八糟的手册和示例代码都完全一样!更离谱的是有一家留的资料链接是第三家店铺的链接,点进去一看是XX电子,可是哥们,你店的名字叫YY科技啊!
最离谱的是有一个非官方的文档(但是写的其实还行),里面有简单介绍了一下 OV7670摄像头模块,看得出来这文档是真正的技术人员写出来的(比如像我这种,写东西就非常的口语化,一看就知道是真敲过代码才写东西的 ),然后我在那个XX电子的文档里看见了几乎一样的(没错,就是我买的YY科技店铺然后给的XX电子的链接里的资料),为什么我说几乎一样呢?因为XX电子还特地把文档里有关这个作者所在公司的部分改成了自己的,你还抄的怪仔细呢。
而且文档里有一段话,我在网上看见非常多次,所以网上这一堆文章都是抄的,起码部分是抄的。这句话就是下面这段,我只截取一部分。
整份文档二十多页,我是怎么记住的呢?因为我觉得这段话有语病,所以被我记下来了(最简单最直接但是最不好实现???为什么简单和不好实现能放一起???既然不好实现就不能算简单吧???)
这一小段“语病”无伤大雅,但是却让我记下来了,所以后面查资料只要出现这段话的,我一律直接关掉当作它是抄来的文章。
顺带一提,如果你想要搜索不到CSDN的页面只需要在你的问题前加上“-csdn ”。
没错,我就是在直接diss你SCDN,抄袭博文在CSDN尤为常见,甚至我还怀疑官方下场去写爬虫搬运,因为我常常能看到一些号,码龄很长,基本都在七八年往上,然后文章非常多,但是基本没有什么原创,都是搬运或者翻译,然后账号在CSDN的排名排到几十万名开外,但是在CSDN写过博文的都知道,但凡你随便写几篇,都不至于排名这么低。
而且在CSDN写博文是没有收益的(不开VIP文的话),因此我觉得不太可能是某些吃饱没事干的人搬运垃圾,所以答案显而易见了。
新的OV7670模块到手之后我很快就开始写程序了,然后很快就歇菜了(其实我很菜的)。
我用的ESP32,用ESP-IDF编写,然后我就发现我还是太菜了,根本采集不到数据(不是ESP32不行,而是我不行),它输出数据实在是太快了,要根据时序来精确采集数据的话有些困难,如果是控制好它输出的频率以及我们采集数据的时间而忽略它的时序的话又要像是炼丹一样去调参数,所以我直接放弃了(我很没耐心的其实)
我选择直接用带FIFO的OV7670,听说非常简单。
然而这个非常简单的带FIFO的OV7670都卡了我三天,我真的是笑嘻嘻了。
中途好几次我都快急哭了,因为时序代码跟网上比对了没问题,但我就是采集不到图像数据。
网上的代码有很多版本,基本没有几个时序相同的,真的是一家一个时序,因此我只好把所有能找到的示例代码都移植一下。
这真的很磨人好吧。
不过最后我还是发现了我采集不到数据的原因了。
我***写了初始化关于FIFO的GPIO的函数了,然后我没有调用,所以读取不到。
人在无语的时候真的会笑。
总之最后我还是搞定了这个OV7670,这是迄今为止我遇到的最麻烦的,耗我时间最长的模块了,我终于释怀地似了。
在吐槽的最后,我要感谢白上咲花老师,是她在我最绝望最无助的时候支持着我,让我勇敢地再度鼓起勇气来面对似乎无法战胜的困难。
SCCB
那么接下来,我们正式开始讲解OV7670。
OV7670一般是这两种版本的,带FIFO(左边)的和不带FIFO(右边)的。
不带FIFO的除了这种经典小蓝板之外还有那种带排线的,我就不去找图了。
然后浅浅给你们看一下效果。
我拍的我的巧克力豆。
因为我用的屏幕是160*128的,所以成像比较小,关于这个屏幕的驱动,我之前有文章写过,可以去回顾一下,里面代码也是可以直接用的。
【硬件模块】ST7735S(1.8寸TFT-LCD)-CSDN博客文章浏览阅读3.7k次,点赞23次,收藏60次。SPI,英文全称Serial Peripheral Interface,即串行外围设备接口,是一种高速、全双工、同步的串行通信总线。我们之前说过I2C,那么我们就拿I2C和SPI做个对比。SPI和I2C对比,优势在于SPI的传输速率比I2C快得多,劣势在于SPI需要用的通信线比较多。SCK(Serial Clock):串行时钟线,由主设备产生,用于同步数据传输。MOSI(Master Output Slave Input):主机输出从机输入线,主设备通过这条线发送数据给从设备。_st7735shttps://blog.csdn.net/m0_63235356/article/details/139422591那么FIFO是什么意思呢?就是First In First Out,先进先出,是不是想到数据结构的队列了?
其实就是一个缓冲区,一般配的缓冲区(存储芯片)是AL422,这个我们后面再说。
因为OV7670是采集图像数据的,而图像数据是非常大的,比如说我们看电视的分辨率1080p就是1920*1080个像素,假设用的是RGB565格式的像素点,也就是两个字节来表示一个像素。
那么1080p分辨率的视频每帧的图像数据是1920*1080*2Byte ≈ 4MB,而如果要人眼感到流畅的视频起码得25帧,这时候每秒传输的图像数据就是将近100MB(然而我们看视频的时候不会耗费那么多流量,甚至看完一集电视剧也才花费了100+MB的流量,这是因为视频播放平台不是放图片那样一帧帧给我们放的,而是用的视频流格式,而且还有一些算法加成)。
虽然OV7670采集的图像数据没有1080p,但它每秒传输的图像数据还是非常多的,因此它输出的速率也是非常快的,一般的单片机还处理不了。
因此就有了带FIFO的OV7670,我们在模块上额外添加一个存储芯片,通过一些办法让OV7670把数据发到这个存储芯片上,然后我们再从存储芯片读取数据,这个时候我们读取数据就不是从OV7670那里拿了,而是从存储芯片拿,时序是由我们自己决定的,因此读取数据的速率可以由我们自行决定,甚至拿51单片机都可以轻松采集数据。
我们先来看看不带FIFO的,通过不带FIFO的来了解一下OV7670。
虽然我没有搞成不带FIFO的,但是思路我还是知道的,没有搞成只是我懒得去调参,我把原理告诉你们之后,保不齐你们就搞出来了呢?I believe you,baby!
我们先来看看它的引脚。
3.3V和DGND(可以直接看成是GND,关于这些,我之前有篇文章介绍过,可以往前翻一翻)提供电源。
这边提一嘴,OV7670实际上需要的是2.8V和1.8V供电,我们可以给3V3的原因是这个模块上有两个LDO帮我们转电平。
如果你买了只有排线的OV7670要自己画板,那就要自己去查看手册来设计OV7670的供电了。
SCL和SDA是时钟线和数据线,这两个很容易让我们想到I2C对吧,实际上它是SCCB,是这个OV公司自己整的通信协议,不过SCCB和I2C其实很像,我们后面再说。
我们通过这两引脚使用SCCB来对OV7670进行寄存器的配置,读取图像数据就和这俩无关了。
也就是说OV7670的配置寄存器和输出图像数据是分开的。
VS和HS,分别是帧同步信号和行同步信号,这个后面再说。
PLK和XLK,分别是像素时钟和系统时钟,对于OV7670来说,PLK是输出信号,而XLK是输入信号,所以XLK是我们要给它输入的信号。
如果我们要让一个芯片工作,就要给它提供时钟信号,对于OV7670来说,XLK就是让它工作的时钟信号,所以如果你用的是不带FIFO的OV7670,那么一定要给它XLK,一定要啊!!!血的教训。
一般来说我们给他占空比为50%的PWM就可以充当时钟信号,而不用单独整一个晶振(自己画板的同学注意了)。
那么给它多少频率的PWM呢?
10MHz ~ 48MHz都可以,最好是24MHz,如果不行,那么也最好是4MHz的倍数,带FIFO的模块是给了个12MHz的晶振,乐鑫官方组件里是推荐ESP32S3给16MHz。
D0~D7都是输出的数据,一共8位,对应这一个字节的数据。
没错,OV7670输出图像数据是并行传输。
RET是复位引脚,当它低电平的时候OV7670复位,一般工作的时候我们就给它高电平。
PWDN是工作模式引脚,不过我觉得就是使能引脚,低电平工作,高电平不工作,一般就给它接低电平。
引脚介绍完,我们先来看看如何使用SCCB以及如何配置OV7670的寄存器吧。
下图是SCCB的时序图。
你以为我会对着这个时序图来讲吗?我才不会!我自己都懒得看这种让人头大的东西(上面那个头)。
其实SCCB和I2C很像,可以说SCCB是阉割版的I2C,所以理论上我们直接使用I2C都可以无缝衔接SCCB。
我稍微讲一下SCCB的时序你们就知道了。
首先主机发起一个起始时序,然后发送从机地址+写,接着发送寄存器地址,最后再发送要写入的数据,再来个结束时序。这就是写寄存器的过程。
读寄存器则是,起始时序,从机地址+写,寄存器地址,结束时序(这里是和I2C不一样的地方,I2C不需要这一步的结束时序),起始时序,从机地址+读,读取一个字节,发送一个nack信号(不应答),结束时序。
通过上面的步骤,相信大家已经对SCCB有了初始的了解了,这货就是华强北版的I2C。
我下面直接给出代码(ESP-IDF),大家看着移植一下就行,真的和I2C很像!里面的DELAY_TIME是宏定义,我通过这个宏定义来调整SCCB的速率,一般给个50就行,表示延时50us。
网上有的版本延时了500us,我看了都觉得离谱,倒也不必这么慢。
void SetSDA(uint8_t val){
gpio_set_level(OV_SDA, val);
usleep(DELAY_TIME);
}
void SetCLK(uint8_t val){
gpio_set_level(OV_SCL, val);
usleep(DELAY_TIME);
}
// 起始信号
// SCL高电平时 SDA高->低
void SCCB_Start(void){
SetSDA(1);
SetCLK(1);
SetSDA(0);
SetCLK(0);
}
// 结束时序
// SCL高电平时 SDA低->高
void SCCB_Stop(void){
SetSDA(0);
SetCLK(1);
SetSDA(1);
}
// NA时序
// SDA高 SCL高 SCL低 SDA低
void SCCB_SendNoAck(void){
SetSDA(1);
SetCLK(1);
SetCLK(0);
SetSDA(0);
}
// SCCB发送一个字节
// 成功返回0,失败返回1
uint8_t SCCB_SendByte(uint8_t data){
for(uint8_t i = 0; i < 8; i++){
SetSDA(data&(0x80 >> i));
SetCLK(1);
SetCLK(0);
}
SetCLK(1);
uint8_t ret = gpio_get_level(OV_SDA);
SetCLK(0);
return ret;
}
// SCCB读取一个字节
uint8_t SCCB_ReadByte(void){
uint8_t ret = 0;
for(uint8_t i = 0; i < 8; ++i){
SetCLK(1);
if(gpio_get_level(OV_SDA)) ret |= (0x80 >> i);
SetCLK(0);
}
return ret;
}
// SCCB写寄存器 成功返回0,失败返回1
// 发送从机地址+寄存器地址+写入内容
uint8_t SCCB_W_Reg(uint8_t add, uint8_t data){
SCCB_Start();
if(SCCB_SendByte(OV7670_W_ADD)){ // 发送OV7670写地址0x42
SCCB_Stop();
return 1;
}
if(SCCB_SendByte(add)){ // 发送寄存器地址
SCCB_Stop();
return 1;
}
if(SCCB_SendByte(data)){ // 发送要写的内容
SCCB_Stop();
return 1;
}
SCCB_Stop();
return 0;
}
// SCCB读寄存器 失败返回0xFF,成功返回读取到的值
uint8_t SCCB_R_Reg(uint8_t add){
uint8_t ret = 0;
SCCB_Start();
if(SCCB_SendByte(OV7670_W_ADD)){ // 发送OV7670写地址
// printf("send w add error\r\n");
SCCB_Stop();
return 0xFF;
}
if(SCCB_SendByte(add)){ // 发送寄存器地址
// printf("send reg add error\r\n");
SCCB_Stop();
return 0xFF;
}
SCCB_Stop();
SCCB_Start();
if(SCCB_SendByte(OV7670_R_ADD)){ // 发送OV7670读地址
// printf("send r add error\r\n");
SCCB_Stop();
return 0xFF;
}
ret = SCCB_ReadByte(); // 读取寄存器数据
SCCB_SendNoAck();
SCCB_Stop();
return ret;
}
OV7670的写从机地址是0x42,读从机地址是0x43。
有了上面的SCCB,那么我们就可以来修改OV7670的寄存器了,通过配置寄存器来调整OV7670输出的图像。
我们首先需要看看0x12这个寄存器。
除了给RST引脚拉低之外,通过给这个寄存器的bit7配置1也可以重置OV7670,并且一开始我们最好给它重置一遍,所以我们先给0x12配置成0x80。
用我上面的代码就是这样的(我就演示一次哈):
SCCB_W_Reg(0x12,0x80)
重置完之后我们再配置寄存器。
不过我们可以先读一下O7670的ID。
在0x0A和0x0B这俩寄存器里,是只读的,并且值永远都是0x76和0x73,这是PID(产品ID)
除了PID之外还有MID(制造商ID)。
我们可以通过读取这两个ID来判断我们的SCCB是否有问题,能否正确读取寄存器(一般能正确读就能正确写,毕竟读比写更麻烦)
uint16_t MID,PID; // MID = 0x7FA2, PID = 0x7673
MID = SCCB_R_Reg(0x1C);MID <<= 8;MID |= SCCB_R_Reg(0x1D);
PID = SCCB_R_Reg(0x0A);PID <<= 8;PID |= SCCB_R_Reg(0x0B);
printf("Get MID is %#x PID is %#x\r\n",MID,PID);
if(MID != 0x7FA2 || PID != 0x7673){
printf("ID error\r\n");
return 1;
}
接下来我们一个个来看OV7670的寄存器,没错,一百多个寄存器我要带你们都看一遍,谁让这是全网最详细的关于OV7670的文章呢。
寄存器
不过大多数情况我们只需要使用商家提供的代码即可,通过修改几个特定的寄存器来达到自定义的效果。
0x00 ~ 0x02 增益调整,0x00我们按照它默认值0x00就行。
蓝色通道和红色通道增益就是值越大,你的图像就越蓝越红。不过我不懂为什么没有绿色通道,不是R(Red红色)G(Green绿色)B(Blue蓝色)嘛。
0x03用来修改帧信号开始与结束的参数,通过修改这个参数我们可以来调整输出图像的高度,这个等后面将图像输出时序的时候再聊。
高两位可以和0x00配合一起调整AGC(自动增益控制)的值。
0x04是COM1,也就是通用控制1,叫这个名字也说明它控制的东西比较杂。
bit6用来使能CCIR656,也就是数字视频接口。
低两位【1:0】是控制AEC(自动曝光控制)的低两位的,其他14位由其他寄存器控制。
0x05、0x06和0x08是什么什么的平均水平,这个无关紧要,虽然是RW(可读可写),但主要还是读的,所以我们可以不用配置。
0x07寄存器的bit0~bit5是AEC(自动曝光控制)的【15:10】,和上面的COM1(0x04)以及另一个寄存器一起控制自动曝光的值的。
0x09是COM2,控制软件睡眠模式以及输出驱动能力的,一般给个0x00。
0x0C是COM3,管的挺杂的,不过它管的东西我们一般都不用,给个0x00就行。
如果需要缩放功能,那么给bit1置一。
COM4的bit5和bit4来调整平均选项(Average option),什么意思我不太懂,这个官方手册说的不清不楚的,烦死了。它说要和COM17的设置一样,看到COM17的时候我们就知道什么意思了。
不过一般我们按照默认值给个0x00就行,我看商家给的例子也是这样的。
COM5全是保留位,不需要管它。
COM6就bit7和bit1有用。
bit7是光学黑行出现的时候要不要HREF(也就是HS,行同步信号),光学黑行是什么我不知道,给它个0吧。
bit1是格式改变的时候要不要重置时序,一般来说我们会在一开始就配置好格式,后面就不会改动了,所以这个bit1随便给就行。
0x10是ACE的【9:2】,和前面的寄存器对上了。
这个寄存器很重要!!!
它的【5:0】控制我们输入时钟的分频,比如说我输入的XLK是12MHz,这边分频设置了0x01(实际上是1 + 1 = 2分频,因为除数不为0,所以基数就是1),那么实际上OV7670得到的时钟频率就是6MHz。
OV7670的时钟频率越快,帧率越高,但如果你不需要那么高的帧率或者帧率太高你处理不过来,那么可以把分频数设高一些。
如果bit6置一了,那么就没有分频,直接使用我们的输入时钟。
0x12这个寄存器也很重要!!!
我们刚才看过的,如果它的bit7置一,那么就会重置OV7670,一开始我们会给它置1,后面我们还要再配一次。
这边配置的是我们的输出大小和格式。
bit5是CIF(352*288)
bit4是QVGA(320*240)
bit3是QCIF(176*144)
一般来说我们会选择QVGA,所以把bit4置一。
bit2和bit0共同控制输出的格式,可以对照上面的参考,一般我们选择RGB,所以bit2置一,bit0置零。
bit1是颜色条???不清楚,我们置0。
所以我们给0x12写入0x14来表示输出QVGA+RGB。
COM8使能一堆东西。
AGC是自动增益控制。
AWB是自动白平衡。
AEC是自动曝光控制。
COM9的【6:4】限制AGC的最大值。
bit0来决定是否固定AGC/AEC。
COM10是来调整时序的,不建议改!
网上一堆时序不一样的,估计就是这里改了。
0x16保留寄存器,不用管。
0x17 ~ 0x1A 用来设置输出图像的大小的,这个我们先不管,后面会单独说。
0x1B设置数据对于行同步信号的延迟,一般不动,就是0x00。
0x1E的bit5设置是是否要镜像(左右翻转),bit4设置是否要上下翻转,一般我们都要。
这样的话你拿着摄像头对准自己的时候,采集的图像就会和你拿手机自拍的图像一样了。
bit2什么黑太阳使能不懂干什么的,不用管。
0x1F保留寄存器,不用管。
0x20,ADC控制,这个看不懂,不管它,让它默认0x04就行。
0x21~0x23和0x29都是保留位,不用管。
0x24和0x25分别设置AGC/AEC的上限和下限,具体是干嘛的怎么设置的我不清楚,我们用商家提供的配置代码就行。
0x27和0x28设置B通道和Gb通道的偏移,这个我也不懂是啥意思,咱不管它。
0x2C是R通道的偏移,同上,不清楚。
0x2A和0x2B设置插入空像素的值,分别是高八位和第八位,这个好像咱不插入空像素,所以这个不管它。
0x2D和0x2E设置插入的空行,这个也不管它。
0x2F是什么Y/G通道平均值,不懂啥意思。
0x30和0x31是设置行同步信号的延迟,按照默认值0x00就行。
0x32这个寄存器来设置输出图像大小的,先不管,后面统一说。
跟输出图像大小的寄存器已经看完了,是0x32,0x17,0x18,0x03,0x19,0x1A。
0x33~0x39这几个寄存器,除了0x35和0x36是保留位,其他的咱都不管它,主要是实在不懂啥意思。
0x3A这个寄存器控制的东西不少。
bit5是否使用负片。
bit4是否使用UV。
bit3和COM13一起决定输出顺序,不过我们使用RGB的,所以这个可以不用管。
bit0设置是否自动输出窗口。
这几个咱都不用管,给个0x00就行。(我的商家提供的是0x04,但是bit2是保留位,不懂它设置这个干嘛)
0x3B,bit7使能夜晚模式。
bit【6:5】决定夜晚模式的帧率是正常的几分之几。
剩下看不懂,反正我们按照默认的给0x00。
0x3C的bit7设置行同步信号是一直都有还是只有帧同步信号为高电平的时候才有,默认是给0,但是我感觉有点奇怪,因为和手册里的时序对不上,但是这么设置却又可以读出数据,真让人摸不着头脑。
0x3D是COM13,看不懂,按照默认配置给就行。
0x3E,按照字面意思,0x3E也是可以让PCLK分频的(这不对吧),然后是管缩放的,但是我们一般按照默认值0x00处理。
0x3F的bit【4:0】调整边缘加强系数 ,不懂怎么调,按照默认值0x00处理。
0x40是COM15,这个需要注意,我们一般是要RGB565格式的,所以bit5和bit4需要调整为0和1。
并且输出的范围越大越好,所以bit7和bit6为1和1。
0x41,COM16,没看懂这些名字是什么意思,所以默认值0x08处理。
0x42,COM17,填一下COM4的坑,这边指的是AEC的窗口大小,两个COM的设置需要一样才会生效,但是没关系,我们全都给0x00,不管它。
0x43~0x4A全都是保留位,不管它。
0x4B使能UV。
0x4C,噪声抑制强度。
0x4D和0x4E都是保留位。
0x4F和0x54都是色彩矩阵系数。
0x55是亮度控制。
0x56是对比度控制。
0x57是对比度中心。
以上参数,手册里具体啥也没说,咱按照默认值给就行。
以上0x58~0x6F,除了0x6B,全部按照默认值给,因为我看不懂,这也太离谱了,摄像头外行哪里看得懂。
我们主要看看0x6B,bit7和bit6控制PLL,也就是让输入时钟信号翻倍,00是不变,01是4倍,10是6倍,11是8倍。
我们就是根据这个0x6B倍频,以及之前的0x11分频来控制PCLK的频率的(输入的是XCLK)。
0x70和0x71的bit7一起控制测试图案。
如果不需要测试的话全部置0,否则按照上面表格里的出测试图案。
建议一开始都测试一下,看看你的摄像头正不正常。
比如我设置的11就是渐变成灰色的彩条,因为设置的是320*240,但是我的TFT比较小,所以我把输出图像大小设置为了160*120,因此彩条显示不全,但是能看出是彩条就行。
0x72设置的是DCW(下采样控制窗口)
0x73控制的是DSP缩放控制。
上面俩按照默认值就行。
0x74设置数字增益。
0x75设置边缘加强的下限。
0x76设置边缘加强的上限,以及使能黑白点校正。
0x77设置噪声去除偏移。
0x78、0x79、0x8A、0x8B都是保留位。
0x7A~0x89都是设置的伽马曲线,具体是啥不清楚,默认值处理。
0x8C,我们需要在bit0置0来失能RGB444,因为我们要RGB565。
剩下一堆寄存器没啥可说的,我倦了~
基本不用管,按照默认值就行。
一般我们只管OV7670采集图像数据就行,图像处理交给后端,但是我们也可以让OV7670输出的图像就是带层滤镜的。
具体怎么调我们可以参考官方的手册OV7670 software application note,商家没提供,我自己上网找的,还让搜狗给我翻译了一下。小伙伴们可以关注我的同名公众号“折途想要敲代码”回复关键词“摄像头”免费下载所有资料,包括我的源代码(不过比较粗糙,因为我改了三天代码已经改的很暴躁了,后续有心情的话再改进再更新)。
OK,我知道那么多寄存器你们跟我一样没什么耐心去看,所以我这边给总结几个比较重要的寄存器,小伙伴们可以自己去单独研究研究,其他寄存器按照商家的代码或者默认值去配就行。
0x03 0x11 0x12 0x17 0x18 0x19 0x1A
0x1E 0x32 0x40 0x6B 0x70 0x71 0x8C
OV7670输出图像数据的时序
寄存器配置完了我们就可以开始读取图像数据了。
回忆一下我们OV7670的引脚,我们用到的是D0~D7,VS,HS,PCLK。
D0~D7八个bit共同组成一个Byte的数据,问题在于我们应该在什么时候读取一个Byte呢?
这就得靠我们的VS帧同步信号,HS行同步信号,PCLK像素时钟信号了。
很遗憾,虽然很不愿意,但是我们还是得看时序图,不过这个时序图还是比较清晰的。
首先是VSYNC,也就是VS帧同步信号。VS一个周期就表示一帧数据,所以我们只需要统计一秒内VS有几个周期就可以得到OV7670的帧率了。
在上图我们可以看的出来,当VS为低电平的时候,HREF(也就是HS行同步信号)开始跳变。
一个VS周期内,HS有几个周期,就表示输出的图像数据的行数有多少,比如说我设置了320*240的图像数据,根据配置PCLk的频率,达到每秒15帧的速率。
那么一秒内,我会有十五个VS的周期,也就是说VS会有十五个上升沿和下降沿。
并且每个VS周期内,都会有240个HS周期,因为图像大小是320*240,图像数据是有240行,所以HS也就会有240个上升沿和下降沿,每秒就会有15*240个HS周期。
理清楚VS和HS的关系之后,我们就要看看HS和PCLK的关系了。
在HS高电平期间,OV7670输出数据。
在VS周期内,HS的第一个高电平期间,OV7670输出的就是第一行的图像数据,第二个高电平期间就是输出第二行的图像数据,以此类推。
与VS和HS不同,PCLK是一直很规律的周期跳变,但是如果PCLK的电平跳变发生在HS高电平期间,那么就需要注意了。
HS高电平期间,PCLK的每个上升沿,我们都可以读取D0~D7;PCLK的每个下降沿,OV7670都会更新D0~D7的数据。
我再总结一下。
在VS低电平期间,表示一帧数据的传输。
在HS高电平期间,表示一帧数据中的一行数据的传输。
PCLK上升沿的时候,我们通过读取D0~D7来获取一个Byte的数据,假设我设置的图像大小是320*240,如果我们用的是RGB565格式,每个像素要两个Byte来表示,那么每个HS的高电平期间,我们可以获取到320 * 2个PCLK上升沿,同时每个VS的低电平期间,我们可以获取到240个HS下降沿。
知道了数据如何传输之后我们就知道如何获取数据了。
我们设置VS引脚的下降沿中断,只要中断触发了就表示一帧数据开始传输,同时开启HS硬件的上升沿中断,只要HS上升沿了,那么表示一行的数据开始传输。
然后再开启PCLK引脚的上升沿中断,PCLK的每个上升沿我们都读取D0~D7来组成一个Byte……
听起来很完美是吗,毕竟如果是320*240的RGB565的15帧图像的话,一秒只有15个VS的中断,3600个HS的中断,但是我的朋友,PCLK的中断次数高达2304000次!(15(15帧)*240(240行像素)*320(每行320个像素)*2(RGB565每个像素两个字节))
也就是说一般的单片机无法处理,否则光搁那中断就行了。
我这边提供个思路,我们只执行VS和HS的中断。
在HS中断触发后我们直接读取D0~D7的数据,只要控制的好,我们可以直接忽视PCLK的信号,然而这需要调参,而且不同的芯片执行程序的速度不一样,也就是说代码没有复用性,所以我直接放弃了。
然后我们来聊聊怎么修改输出图像的大小,正常320*240就差不多可以了,但是我的TFT只有160*128怎么办呢?
正如我们之前说的,修改输出图像大小和6个寄存器相关,分别是
0x32,0x17,0x18,0x03,0x19,0x1A
为什么刚刚将寄存器的时候不说要等现在说呢?因为我们要先了解了输出图像的时序才知道如何配置。
刚刚说了,决定一帧图像有多少行是由一个VS周期内有几个HS周期决定的,而一行数据有多少像素是由在HS高电平器件,有几个PCLK上升沿决定的。
所以理论上我们可以通过单片机的软件来决定输出图像的大小,比如说我是采集了320*240*2个数据,但是我处理一下,只用其中160*120*2个数据,理论上也可以达到修改输出图像大小的效果。
如上图,黑框是我装着采集到的数据的二维数组,红框是我使用的数据(有点抽象,不过抽象思维不好的人也不会选择当程序员对叭~)
这样也有个问题,就是如果我是因为单片机的存储容量太小才选择缩小输出图像大小的呢?这么做的话并不能解决我的问题啊。
那还有一种软件处理方式,就是在每个VS周期里,我忽略掉前80个HS周期和后80个HS周期,然后在每个HS周期里,我忽略掉前60个和后60个PCLK的上升沿,这样也可以达到效果。
最后就是我们通过配置寄存器的方法了,毕竟能让硬件做的活就不要软件来做。
具体如何配置,由于手册写的太抽象,我根本看不懂,所以我不会。
不过我不会,不代表我不会抄,早在2012年就有热心网友整理出了配置寄存器的函数了,我们只要用就行了。
热心网友写了文档,我就不带大家看了,我直接把配置函数放在下面。对那份文档感兴趣的小伙伴可以关注我的同名公众号“折途想要敲代码”回复关键词“摄像头”免费下载所有资料,包括我的源代码(不过比较粗糙,因为我改了三天代码已经改的很暴躁了,后续有心情的话再改进再更新)。
void OV7670_config_window(u16 startx,u16 starty,u16 width, u16 height)
{
u16 endx=(startx+width*2)%784;
u16 endy=(starty+height*2);
u8 x_reg, y_reg;
u8 state,temp;
state = rdOV7670Reg(0x32, &x_reg );
x_reg &= 0xC0;
state = rdOV7670Reg(0x03, &y_reg );
y_reg &= 0xF0;
//设置 HREF
temp = x_reg|((endx&0x7)<<3)|(startx&0x7);
state = wrOV7670Reg(0x32, temp );
temp = (startx&0x7F8)>>3;
state = wrOV7670Reg(0x17, temp );
temp = (endx&0x7F8)>>3;
state = wrOV7670Reg(0x18, temp );
//设置 VREF
temp = y_reg|((endy&0x3)<<2)|(starty&0x3);
state = wrOV7670Reg(0x03, temp );
temp = (starty&0x3FC)>>2;
state = wrOV7670Reg(0x19, temp );
temp = (endy&0x3FC)>>2;
state = wrOV7670Reg(0x1A, temp );
}
接下来我们来看看带FIFO的OV7670。
使用带FIFO的OV7670
我们先来看看引脚。
比不带FIFO的多了几个,也少了几个。
少的是PCLK和XCLK,因为OV7670的数据是给到FIFO里的,所以PCLK我们不用管;因为自带晶振,所以不需要XCLK给提供时钟信号。
多的引脚就是前缀是FIFO的,那个就是用来控制存储芯片的。
这边的D0~D7和不带FIFO的OV7670的D0~D7不一样,这边的是连接到FIFO的,而不带FIFO的是连接到OV7670的,虽然我们都是通过这8根线来获取并行数据的,但是区别还是要说一下的。
VSYNC(VS)、HREF(HS)、RESET(RET)、PWDN和不带FIFO是一样的,不过我们使用FIFO的话,不需要HS,所以可以不接线。
包括STR也可以不接线。
所以我们要学习使用的就是带FIFO的五根线。
首先我们先了解一下FIFO如何工作的。
本质上就是OV7670把数据写进FIFO,我们再从FIFO里读取,所以我们要做的就是控制写和控制读。
因为我们写入和读取的是同一片存储空间,所以写和读要区分开来,在FIFO内就是有两个指针,一个写指针,一个读指针,写指针指向的地址就是写入数据的地址,而读指针指向的地址就是我们读出数据的地址。
我们看下FIFO这个存储芯片的引脚就可以知道,写入数据的引脚和读出数据的引脚是不一样的,所以我们依靠着不同的读写指针和不同的输入输出的并行通道可以实现同时读和写。
一般来说写的速度会比我们读的速度快,因为如果我能用很快的速度去读取,那么我为什么还要用FIFO呢?
因此我们读和写可以同时进行,不用担心会冲突(不过还是要稍微控制一下时序,这个后面会说)
接下来我们看看时序。
第一个是写指针复位,也就是把写指针的指向改成存储单元的起始地址,也就是0。
上面速度时序图中我们只需要管WRST这一个指针就行,因为DI0~DI7是直接接到OV7670的,WCK也是接到OV7670的,顺带提一下FIFO的工作也是要时钟信号的,不过写东西和读东西用的时钟信号是分开来的,这也是为什么我们可以用比较慢的速度去读数据。
WCK就写数据用的时钟信号,直接接到OV7670的PCLK上,我们不需要管。
所以我们要让写指针复位,只需要把WRST拉低再拉高就行,如果不行的话就在中间延时个1us。
// 复位写指针
void FIFO_ResetWPoint(void){
gpio_set_level(FIFO_WRST, 0);
usleep(1);
gpio_set_level(FIFO_WRST, 1);
usleep(1);
}
下一个是读指针复位,和写指针一样,也是把RRST拉低再拉高,不一样的是RCK是需要我们来控制的,可以看到我们需要两个RCK周期才能完成读指针的复位,并且第RCK二个周期的上升沿是在RRST拉高之后,所以代码是下面这样的。
// 复位读指针
void FIFO_ResetRPoint(void){
gpio_set_level(FIFO_RRST, 0);
gpio_set_level(FIFO_RCK, 0);
gpio_set_level(FIFO_RCK, 1);
gpio_set_level(FIFO_RCK, 0);
gpio_set_level(FIFO_RRST, 1);
gpio_set_level(FIFO_RCK, 1);
}
RCK的第二次拉高要在RRST拉高之后,这一点很重要,否则出来的图像是花屏。
上面俩时序我们一起看,一个是允许被读,一个是允许输出,我是觉得有点多此一举了,你不输出我怎么读,我要读的话你肯定要输出啊。
总之我们得到的信息就是OE和RE都要拉低,我们才能读取数据。
细心的小伙伴可能发现了,我们上面引脚介绍里没有RE引脚,这是因为正如我所说,两个一起控制我们读取数据多此一举了,所以在硬件模块上,直接在硬件层面永久拉低RE了,所以我们只需要控制OE就可以使能读取数据,后面我们再一起看看带FIFO的OV7670摄像头模块的原理图。
void FIFO_OpenReadData(void){
gpio_set_level(FIFO_OE, 0); // 允许写入
}
void FIFO_CloseReadData(void){
gpio_set_level(FIFO_OE, 1); // 禁止写入
}
除了控制OE之外,我们在上面俩时序图中还能看出我们应该如何读取数据。
我们看看RCK,我们在RCK的上升沿读取数据,在RCK下降沿的时候,FIFO会把数据放在八个输出引脚上,由于RCK是我们控制的,所以我们可以很轻松地读取数据。
每次读取数据我们要做的就是先把写指针复位,然后拉低OE使能读取,然后要读几个字节我们就来几个RCK的周期。
比如说我们是320*240的RGB565,那么我们读取一帧数据就是要来153600个字节的数据,顺带一提,我们常用在OV7670的AL422,它的容量是384kB,所以基本上是够用的。
void FIFO_ReadData(uint8_t* cache, uint16_t len){
FIFO_ResetRPoint();
FIFO_OpenReadData();
for(uint16_t index = 0; index < len; ++index){
gpio_set_level(FIFO_RCK, 1);
cache[index] = (gpio_get_level(FIFO_D0)) | (gpio_get_level(FIFO_D1) << 1) | (gpio_get_level(FIFO_D2) << 2) |
(gpio_get_level(FIFO_D3) << 3) | (gpio_get_level(FIFO_D4) << 4) | (gpio_get_level(FIFO_D5) << 5) |
(gpio_get_level(FIFO_D6) << 6) | (gpio_get_level(FIFO_D7) << 7);
gpio_set_level(FIFO_RCK, 0);
}
FIFO_CloseReadData();
}
然后关于写数据的时序我们简单看看就行,因为基本上不用我们管,我们只需要看准时机允许写就行。
写和读类似,也是在WCK时钟信号的上升沿写入数据。
我们要做的就是使能写,也就是拉低WE,但是我们要仔细看看我们的模块,因为模块上很有可能集成了一个与非门,导致我们要拉高WE才能拉低WE(很有哲理的一句话对吧)。
我们看看原理图。
FIFO的WE是与非门的输出端,输入端是我们模块上的WR和接到OV7670的HREF,也就是说当我们拉高WR并且OV7670的HREF为高的时候,才会允许FIFO写入。
我们再回想一下OV7670的时序,当OV7670拉低VS的时候开始一帧数据的传输,而这时候拉高HS(HREF)则表示一行数据的传输,所以HS高电平的时候输出的数据才是有效的图像数据。
因此这边加个与非门是很有必要的。
这边提一下B站搜索OV7670得到的播放量最高的那个视频里面的老师说的其实是错误的,但是他的代码又歪打正着的能用,当然了,他的评论区也指出了这一问题。
鉴于OV7670的资料那么难找,b站最有用的视频又有错误,所以我决定后续找个时间自己录制一期关于OV7670的视频,具体什么时候看我心情,因为我短期内实在是不想再碰OV7670这玩意了。
所以我们要控制FIFO写入数据,就是在VS下降沿(其实上升沿也可以,因为控制允许写入的还有HS)的时候拉高WR,然后复位写指针(二者的顺序可以替换,我觉得无所谓),等到VS的下一个下降沿的时候表示新的一帧数据来了,同时表示上一帧数据结束,所以我们可以禁止写入,然后开始读取数据。
下面是我关于ESP-IDF的代码,仅供参考。
// VS帧同步信号中断处理函数
void IRAM_ATTR OV_vs_function(void *arg){
vs_flag++;
if(vs_flag == 1){
FIFO_ResetWPoint();
FIFO_OpenWriteData();
}else if(vs_flag == 2){
gpio_intr_disable(OV_VS); // 暂时关闭中断,防止读和写冲突
FIFO_CloseWriteData(); // 关闭写允许
FIFO_ReadData(OV_Data_Cache, 38400); // 开始读取数据,这边可以改为直接把数据送到显示屏
xSemaphoreGiveFromISR(vs_begin_sem, &temp); // 可以把读取到的存在MCU的数据送到显示屏
vs_flag = 0;
gpio_intr_enable(OV_VS); // 开启中断
}
}
这边提出几个可以优化的点。
第一是我读数据和把数据传到TFT屏幕是分开来的,实际上我们可以直接把读到的数据传到TFT上,这么做有两个好处,第一个是总耗时会更短,第二是我们不需要划拉出一大块空间来存储临时数据。
第二是我驱动TFT用的是软件SPI,这边改成硬件SPI会快很多,如果加上DMA的话又会很快。
第三是上面的代码属于是自己把帧率砍一大半了,因为我放弃了第二帧的数据,甚至在中断里执行了很多耗时的操作,实际上如果读取的速度够快(实现上面两点),那么我们不需要放弃第二帧数据,我们可以一边让他写入数据我们一边读,由于我们读的速度小于它写入的速度,所以是不会冲突的。
顺带一提我在中断里执行这一大堆内容的行为不提倡,因为我把ESP-IDF自带的任务看门狗和中断看门狗都给关了。
但是我懒得改了,你们自己去优化去吧,我这么做大概是每秒两三帧这样。
我会把所有的资料和我的代码(基于ESP-IDF,包括能用的带FIFO的以及不带FIFO的不能用的)都放我公众号里,小伙伴们可以关注下我的同名公众号“折途想要敲代码”私信回复关键词“摄像头”即可免费下载。
我的代码仅供参考(写了不少注释,结合我的文章你们应该看得懂),因为我已经改到崩溃了,短时间内不想去看它了,但确实是能用的(带FIFO的是能用的,不带FIFO的纯仅供参考)。
后面我看看找时间改进一下再放上去吧,具体什么时候看我心情。