1、STM32图像接收接口
使用stm32芯片,128kB RAM,512kB Rom,资源有限,接摄像头采集图像,这种情况下,内存利用制约程序设计。
STM32使用DCMI接口读取摄像头,协议如下。行同步信号指示了一行数据完成,场同步信号指示了一帧图像传输完成。所以出现了两种典型的数据接收方式,按照行信号一行一行处理,按照场信号一次接收一副图像。
2、按行读取
以网络上流行的野火的demo为例,使用行中断,用DMA来读取一行数据。
//记录传输了多少行 static uint16_t line_num =0; //DMA传输完成中断服务函数 void DMA2_Stream1_IRQHandler(void) { if ( DMA_GetITStatus(DMA2_Stream1,DMA_IT_TCIF1) == SET ) { /*行计数*/ line_num++; if (line_num==img_height) { /*传输完一帧,计数复位*/ line_num=0; } /*DMA 一行一行传输*/ OV2640_DMA_Config(FSMC_LCD_ADDRESS+(lcd_width*2*(lcd_height-line_num-1)),img_width*2/4); DMA_ClearITPendingBit(DMA2_Stream1,DMA_IT_TCIF1); } } //帧中断服务函数,使用帧中断重置line_num,可防止有时掉数据的时候DMA传送行数出现偏移 void DCMI_IRQHandler(void) { if ( DCMI_GetITStatus (DCMI_IT_FRAME) == SET ) { /*传输完一帧,计数复位*/ line_num=0; DCMI_ClearITPendingBit(DCMI_IT_FRAME); } }
DMA中断服务函数中主要是使用了一个静态变量line_num来记录已传输了多少行数据,每进一次DMA中断时自加1,由于进入一次中断就代表传输完一行数据,所以line_num的值等于lcd_height时(摄像头输出的数据行数),表示传输完一帧图像,line_num复位为0,开始另一帧数据的传输。line_num计数完毕后利用前面定义的OV2640_DMA_Config函数配置新的一行DMA数据传输,它利用line_num变量计算显存地址的行偏移,控制DCMI数据被传送到正确的位置,每次传输的都是一行像素的数据量。
当DCMI接口检测到摄像头传输的帧同步信号时,会进入DCMI_IRQHandler中断服务函数,在这个函数中不管line_num原来的值是什么,它都把line_num直接复位为0,这样下次再进入DMA中断服务函数的时候,它会开始新一帧数据的传输。这样可以利用DCMI的硬件同步信号,而不只是依靠DMA自己的传输计数,这样可以避免有时STM32内部DMA传输受到阻塞而跟不上外部摄像头信号导致的数据错误。
图像按帧读取比按行读取效率更高,那么为什么要按行读取呢?上面的例子是把图像送到LCD,如果是送到内存,按帧读取就需要芯片有很大的内存空间。以752*480的分辨率为例,需要360kB的RAM空间,远远超出了芯片RAM的大小。部分应用不需要摄像头全尺寸的图像,只需要中心区域,比如为了避免畸变影响一般只用图像中间的部分,那么按行读取就有一个好处,读到一行后,可以把不需要的丢弃,只保留中间部分的图像像素。
那么问题来了?为什么不直接配置摄像头的属性,来实现只读取图像的中间部分呢,全部读取出来然后在arm的内存中裁剪丢弃不要的像素,第一浪费了读取时间,第二浪费了读取的空间。更优的做法是直接配置摄像头sensor,使用sensor的裁剪功能输出需要的像素区域。
3、图像裁剪--使用STM32 crop功能裁剪
STM32F4系列的DCMI接口支持裁剪功能,对摄像头输出的像素点进行截取,不需要的像素部分不被DCMI传入内存,从硬件接口一侧就丢弃了。
HAL_DCMI_EnableCrop(&hdcmi); HAL_DCMI_ConfigCrop(&hdcmi, CAM_ROW_OFFSET, CAM_COL_OFFSET, IMG_ROW-1, IMG_COL-1);
裁剪的本质如下所述,从接收到的数据里选择需要的矩形区域。所以STM32 DCMI裁剪功能可以完成节约内存,只选取需要的图像存入内存的作用。
此方法相比于一次读一行,然后丢弃首尾部分后把需要的区域图像像素存入buffer后再读下一行,避免了时序错误,代码简洁了,DCMI硬件计数丢掉不要的像素,也提高了程序可靠性、可读性。
成也萧何败也萧何,如上面所述,STM32的crop完成了选取特定区域图像的功能,那么也要付出代价,它是从接收到的图像数据里进行选择的,这意味着那些不需要的数据依然会传输到MCU一侧,只不过MCU的DCMI对数据进行计数是忽略了它而已,那么问题就来了,哪些不需要的数据的传输会带来什么问题呢?
有图为证,下图是使用了STM32 crop裁剪的时序图,通道1启动采集IO置高,frame中断里拉低,由于使用dma传输,那么被crop裁剪后dma计数的数据量变少,所以DCMI frame中断能在行数据传输完成前到达,通道1高电平部分就代表一有效分辨率的帧的采集时间。通道2 曝光信号管脚,通道3是行扫描信号。其中通道1下降沿到通道3下降沿4.5ms。代表单片机已经收到crop指定尺寸的图像,采集有效区域(crop区域)的图像完成,但是line信号没有结束还有很多行没传输,即CMOS和DCMI接口要传输752*480图像还没完成。
举例说明,如果使用752*480分辨率采集图像,你只取中间的360*360视野,有效分辨率是360*360,但是总线上的数据依然是752*480,所以帧率无法提高,多余的数据按说就不应该传输出来,如何破解,问题追到这里,STM32芯片已经无能为力了,接下来需要在CMOS一侧发力了。
4、图像裁剪--配置CMOS寄存器裁剪
下图是MT9V034 摄像头芯片的寄存器手册,Reg1--4配置CMOS的行列起点和宽度高度。
修改寄存器后,摄像头CMOS就不再向外传输多余的数据,被裁剪丢弃的数据也不会反应在接口上,所以STM32 DCMI接收到的数据都是需要保留的有效区数据,极大地减少了数据输出,提高了传输效率。本人也在STM324芯片上,实现了220*220分辨率120帧的连续采集。
下面是序图,通道1高电平代表开始采集和一帧结束,不同于使用STM32 的crop裁剪,使用CMOS寄存器裁剪有效窗口,使得帧结束时行信号也同时结束,后续没有任何需要传输的行数据。
5、一帧数据一次性传输
一帧数据一次全部读入到MCU的方式,其实是最简单的驱动编写方式,缺点就是太占内存,但是对于没有压缩功能的cmos芯片来说,一般都无力实现。对部分有jpg压缩功能的cmos芯片而言,比如OV2640可以使用这种方式,一次性读出一帧图像。
__align(4) u32 jpeg_buf[jpeg_buf_size]; //JPEG buffer //JPEG 格式 const u16 jpeg_img_size_tbl[][2]= { 176,144, //QCIF 160,120, //QQVGA 352,288, //CIF 320,240, //QVGA 640,480, //VGA 800,600, //SVGA 1024,768, //XGA 1280,1024, //SXGA 1600,1200, //UXGA };
//DCMI 接收数据
void DCMI_IRQHandler(void)
{
if(DCMI_GetITStatus(DCMI_IT_FRAME)==SET)// 一帧数据
{
jpeg_data_process();
DCMI_ClearITPendingBit(DCMI_IT_FRAME);
}
}