开源ISP介绍(2)————嵌入式Vitis搭建

发布于:2024-12-06 ⋅ 阅读:(24) ⋅ 点赞:(0)

Vivado搭建参考前一节Vivado基于IP核的视频处理框架搭建:

开源ISP介绍(1)——开源ISP的Vivado框架搭建-CSDN博客

导出Hardware

在vivado中导出Hardware文件,成功综合—实现—生成比特流后导出硬件.xsa文件。(注意导出时选择include bitstream选项)

.xsa是用于在Vitis中生成平台(Platform)的文件,平台可以看作是BSP板级支持包文件,嵌入式Vitis使用了分层的设计架构。

Platform工程

BSP文件如FPGA中使用的所有硬件用到的驱动文件(包括PS端的MCU、各个Xilinx官方IP的驱动以及自定义IP的驱动),创建平台后会在Vitis平台目录下生成一个比较重要的描述PS端外设信息的xparameters.h(xparameters_ps.h是描述PS端外设信息的文件)头文件,该文件中描述了Vivado BolockDesign综合后的IP基地址、器件、中断号等信息。

例如用到的AXI IIC IP核,在该文件的描述如下

后续在配置该外设时,就可以通过官方提供的XIic驱动函数XIic_LookupConfig查找器件ID,并得到该IP相应的基本地址等信息(该过程类似于Linux中的设备树文件解析)与配置结构体。

 然后使用对应的配置驱动函数XIic_CfgInitialize对IP进行配置,如果有中断还需要配置中断相关的内容,Xinlix IP的配置大致都遵循这个步骤。

以上简单介绍了通过Vivado导出的.xsa文件在Vitis生成相关的BSP平台文件过程。

嵌入式Vitis源码分析

接下来创建一个Application Project

并将开源项目提供的src文件中的内容复制到Application项目中,阅读从main函数开始,大致流程为:配置系统中断——配置GPIO——配置SD文件系统挂载——配置摄像头——配置ISP视频处理相关——配置VDMA帧缓存——While循环,内容如下

int main()
{
	XAxiVdma cam_vdma_inst;
	XAxiVdma lcd_vdma_inst;
	XAxiVdma dvi_vdma_inst;
	IspContext isp_context = {0};

	sys_intr_init();    //配置中断相关
	gpio_init();        //配置GPIO相关

	fs_init = 0 == platform_init_fs();

	u32 status;
	//OV5640设置为DVP输出,设置输出的像素长宽
	u16 cmos_h_pixel = CAM_WIDTH;    //ov5640 DVP 输出水平像素点数
	u16 cmos_v_pixel = CAM_HEIGHT;   //ov5640 DVP 输出垂直像素点数
	u16 total_v_std = 0;

	status = ov5640_init(cmos_h_pixel, cmos_v_pixel, &total_v_std); //初始化ov5640,配置相关寄存器参数
	if(status == 0)
		xil_printf("OV5640 detected successful!\r\n");
	else
		xil_printf("OV5640 detected failed!\r\n");

	xil_printf("cmos size %u x %u\r\n", cmos_h_pixel, cmos_v_pixel);
	cmos_set_exposure(total_v_std);   //设置曝光AEC
	cmos_set_gain(0x200);             //设置增益AGC

	isp_context.base = ISP_BASE;
	isp_context.pfn_set_exposure = _set_exposure;
	isp_context.pfn_set_gain = _set_gain;
	isp_context.priv_data = NULL;
	isp_context.ae_target_luminance = 75<<(ISP_BITS-8);
	isp_context.ae_max_exposure = total_v_std;
	isp_context.ae_max_gain = 0x1ff;   //512
	isp_context.ae_cur_exposure = total_v_std;
	isp_context.ae_cur_gain = 0x010;
	isp_context.ae_cur_isp_dgain = 0x010;
	isp_context.awb_cur_rgain = 0x010;
	isp_context.awb_cur_bgain = 0x010;

	init_camif_isp_vip();  //配置相关IP

	//配置VDMA的帧缓存
	run_triple_frame_buffer(&cam_vdma_inst, XPAR_AXIVDMA_0_DEVICE_ID,CAM_WIDTH, CAM_HEIGHT, cam_buff, 0, 0, BOTH);    //RAW
	run_triple_frame_buffer(&lcd_vdma_inst, XPAR_AXIVDMA_1_DEVICE_ID,LCD_WIDTH, LCD_HEIGHT, lcd_buff, 0, 0, BOTH);    //LCD
	run_triple_frame_buffer(&dvi_vdma_inst, XPAR_AXIVDMA_2_DEVICE_ID,DVI_WIDTH, DVI_HEIGHT, dvi_buff, 0, 0, BOTH);    //DVI

	printf("initialize ok\r\n");

	unsigned prev_frame_cnt = 0, prev_cam_int = 0, prev_isp_int = 0, prev_vip_int = 0;
	u64 prev_time = 0;
	XTime_GetTime(&prev_time);
	printf("prev_time %llu\n", prev_time);
	while(1) {
		u64 now_time = 0;
		do {
			unsigned curr_isp_int = isp_frame_int;
			XTime_GetTime(&now_time);
			while (curr_isp_int == isp_frame_int && now_time < prev_time + COUNTS_PER_SECOND)
			{
				XTime_GetTime(&now_time);
			}
			//if (isp_frame_int % 2 == 0) {
				isp_ae_handler(&isp_context);    //AE处理
				isp_awb_handler(&isp_context);   //AWB处理
			//}
		} while (now_time < prev_time + COUNTS_PER_SECOND);
		prev_time = now_time;

#define CYCLE_DEBUG_PRINT 0
		unsigned frame_cnt = XIL_CAMIF_mReadReg(CAMIF_BASE, CAMIF_REG_FRAME_CNT);
		unsigned cam_int = cam_frame_int, isp_int = isp_frame_int, vip_int = vip_frame_int;
#if CYCLE_DEBUG_PRINT
		printf("%lu x %lu, fps %u, interrupt camif %u, isp %u, vip %u\n",
				XIL_CAMIF_mReadReg(CAMIF_BASE, CAMIF_REG_WIDTH),
				XIL_CAMIF_mReadReg(CAMIF_BASE, CAMIF_REG_HEIGHT),
				frame_cnt - prev_frame_cnt,
				cam_int - prev_cam_int,
				isp_int - prev_isp_int,
				vip_int - prev_vip_int);
#endif
		prev_frame_cnt = frame_cnt;
		prev_cam_int = cam_int;
		prev_isp_int = isp_int;
		prev_vip_int = vip_int;

#if CYCLE_DEBUG_PRINT
		unsigned i, sum;
		printf("AEC HIST [");
		for (sum = 0, i = 0; i < ISP_REG_STAT_AE_HIST_SIZE; i+=4) {
			unsigned data = XIL_ISP_LITE_mReadReg(ISP_BASE, ISP_REG_STAT_AE_HIST_ADDR+i);
			sum += data;
			if (i >= 96*4 && i < 96*4 + 8*4)
				printf("%u, ", data);
		}
		printf("] total %u\n", sum);//sum may be error, because of reading hist in vsync time
		printf("AWB HIST [");
		for (sum = 0, i = 0; i < ISP_REG_STAT_AWB_HIST_SIZE; i+=4) {
			unsigned data = XIL_ISP_LITE_mReadReg(ISP_BASE, ISP_REG_STAT_AWB_HIST_ADDR+i);
			sum += data;
			if (i >= 96*4 && i < 96*4 + 8*4)
				printf("%u, ", data);
		}
		printf("] total %u\n", sum);//sum may be error, because of reading hist in vsync time
#endif
		key_control();    //按键控制
	}

	return 0;
}

配置中断

在Vivado的设计中,将所有IP生成的中断信号通过一个中断控制IP核集中为一个然后接到了PS端的PS-PL中断上

在parameters.h中生成了该IP接入的中断号及其屏蔽位,这些中断统一由AXI Interrupt Controller这个IP核接管,然后输出一路irq接到GIC中断控制器上

在中断配置中需要将该IP核的中断与GIC中断进行连接,sys_intr_init()函数内容如下:

static XIntc axiIntc;
static XScuGic intc;


int sys_intr_init(void)
{
    XIntc_Initialize(&axiIntc, XPAR_AXI_INTC_0_DEVICE_ID);     //初始化AXI_INTC器件
    XIntc_Start(&axiIntc, XIN_REAL_MODE);

    int status;
    XScuGic_Config *intc_cfg;
    intc_cfg = XScuGic_LookupConfig(XPAR_SCUGIC_SINGLE_DEVICE_ID);   //初始化GIC中断控制器
    if (NULL == intc_cfg) {
        return XST_FAILURE;
	}
    status = XScuGic_CfgInitialize(&intc, intc_cfg, intc_cfg->CpuBaseAddress);    //配置GIC
    if (status != XST_SUCCESS)
        return XST_FAILURE;

    XScuGic_Connect(&intc, XPS_FPGA0_INT_ID, (Xil_ExceptionHandler) XIntc_InterruptHandler, (void*)&axiIntc);    //连接中断
    XScuGic_Enable(&intc, XPS_FPGA0_INT_ID);

    Xil_ExceptionInit();          //使能异常相关
    Xil_ExceptionRegisterHandler(XIL_EXCEPTION_ID_INT, (Xil_ExceptionHandler)XScuGic_InterruptHandler, (void*)&intc);
    Xil_ExceptionEnable();

    return XST_SUCCESS;
}

AXI Interrupt Controller与GIC已经连接上了,后续具体IP的中断配置只需要通过AXI Interrupt Controller提供的驱动函数XIntc_Connect进行连接就行。

GPIO配置

ZYNQ7000系列芯片有54个MIO,隶属于PS部分,使用时不需要添加引脚约束,对PL部分是不可见的分为Bank0与Bank1,可以接许多外设比如UART、SPI或GPIO等,另外可以引脚复用。在使用PS端MIO时需要对GPIO进行配置,这个配置过程与普通的ARM芯片过程一致。

而EMIO从物理属性来说属于PL端IO,但PS端可以通过软件映射连线来对EMIO进行操作,且使用效果与MIO一致,EMIO需要管脚约束。

因此MIO编号范围为0~53,EMIO编号范围为54~117

在Vivado中,配置了使能了MIO(PS端)和EMIO(PL端),并将相关的管脚配置到的PS与PL端的按键和LED灯上。(EMIO就是可扩展的MIO,当与PS直接相连的MIO不够用时,可以使用EMIO做“扩展”。)

通过查找正点原子对应IO引脚分配表,可以直接得到PS端GPIO按键和LED灯的编号,因为PS端的MIO是无需管脚约束,管脚与MIO映射都是对应好的。

而PL端是需要管脚约束的,如下,EMIO的位宽配置了5(0~4),但EMIO的编号是从54开始的,因此依次对应下去就是54、55、56、57、58五个EMIO编号。

因此在Vitis中声明了如下GPIO编号

GPIO的配置如下:

static void gpio_init()
{
	XGpioPs_Config *gpiops_cfg_ptr; //PS 端 GPIO 配置信息

	//根据器件 ID 查找配置信息
	gpiops_cfg_ptr = XGpioPs_LookupConfig(GPIOPS_ID);
	//初始化器件驱动
	XGpioPs_CfgInitialize(&gpiops_inst, gpiops_cfg_ptr, gpiops_cfg_ptr->BaseAddr);

	//Gpio LED
	XGpioPs_SetDirectionPin(&gpiops_inst, PS_LED0, 1);     //设置为输出
	XGpioPs_SetDirectionPin(&gpiops_inst, PS_LED1, 1);
	XGpioPs_SetDirectionPin(&gpiops_inst, PL_LED0, 1);
	XGpioPs_SetDirectionPin(&gpiops_inst, PL_LED1, 1);
	XGpioPs_SetOutputEnablePin(&gpiops_inst, PS_LED0, 1);  //使能使出
	XGpioPs_SetOutputEnablePin(&gpiops_inst, PS_LED1, 1);
	XGpioPs_SetOutputEnablePin(&gpiops_inst, PL_LED0, 1);
	XGpioPs_SetOutputEnablePin(&gpiops_inst, PL_LED1, 1);
	XGpioPs_WritePin(&gpiops_inst, PS_LED0, 1);            //设置输出高电平
	XGpioPs_WritePin(&gpiops_inst, PS_LED1, 1);
	XGpioPs_WritePin(&gpiops_inst, PL_LED0, 1);
	XGpioPs_WritePin(&gpiops_inst, PL_LED1, 1);

	//Gpio KEY
	XGpioPs_SetDirectionPin(&gpiops_inst, PS_KEY0, 0);   //设置为输入
	XGpioPs_SetDirectionPin(&gpiops_inst, PS_KEY1, 0);
	XGpioPs_SetDirectionPin(&gpiops_inst, PL_RESET, 0);
	XGpioPs_SetDirectionPin(&gpiops_inst, PL_KEY0, 0);
	XGpioPs_SetDirectionPin(&gpiops_inst, PL_KEY1, 0);
}

挂载SD卡文件系统

代码如下

static int platform_init_fs()
{
	FRESULT status;
	TCHAR *Path = "0:/";
	BYTE work[FF_MAX_SS];

    //注册一个工作区(挂载分区文件系统)
    //在使用任何其它文件函数之前,必须使用f_mount函数为每个使用卷注册一个工作区
	status = f_mount(&fatfs, Path, 1);  //挂载SD卡
	if (status != FR_OK) {
		xil_printf("Volume is not FAT formated; formating FAT\r\n");
		return -1;
//		//格式化SD卡
//		status = f_mkfs(Path, FM_FAT32, 0, work, sizeof work);
//		if (status != FR_OK) {
//			xil_printf("Unable to format FATfs\r\n");
//			return -1;
//		}
//		//格式化之后,重新挂载SD卡
//		status = f_mount(&fatfs, Path, 1);
//		if (status != FR_OK) {
//			xil_printf("Unable to mount FATfs\r\n");
//			return -1;
//		}
	}
	return 0;
}


//SD卡写数据
static int sd_write_data(char *file_name,u32 src_addr,u32 byte_len)
{
    FIL fil;         //文件对象
    UINT bw;         //f_write函数返回已写入的字节数

    //打开一个文件,如果不存在,则创建一个文件
    f_open(&fil,file_name,FA_CREATE_ALWAYS | FA_WRITE);
    //移动打开的文件对象的文件读/写指针     0:指向文件开头
    f_lseek(&fil, 0);
    //向文件中写入数据
    f_write(&fil,(void*) src_addr,byte_len,&bw);
    //关闭文件
    f_close(&fil);
    return 0;
}

挂载成功后,就可以直接使用f_open、f_write、f_read等函数直接操作SD卡的文件读写了,例如通过操作f_write将DDR中保存的raw图写到SD卡中。

配置OV5640摄像头

配置Ov5640摄像头需通过SCCB总线(可看作I2C总线的裁剪版),在Vivado设计中使用了一个AXI IIC IP核连接了OV5640的SCCB总线

因此首先就需要配置该IP,配置的过程与Xilinx IP通用配置流程差不多

static int iic_init(void)
{
	int rc;
	//通过axi_iic器件ID查找器件
	XIic_Config *cfg_ptr = XIic_LookupConfig(XPAR_AXI_IIC_DEVICE_ID);
	if (!cfg_ptr) {
		xil_printf("[iic] XIic_LookupConfig() failed\r\n");
		return XST_FAILURE;
	}
    //配置axi_iic IP
	rc = XIic_CfgInitialize(&iic, cfg_ptr, cfg_ptr->BaseAddress);
	if (rc != XST_SUCCESS) {
		xil_printf("[iic] XIic_CfgInitialize() failed\r\n");
		return rc;
	}
	//设置相关中断
	XIic_SetSendHandler(&iic, &iic, (XIic_Handler) SendHandler);           //设置相关中断处理函数
	XIic_SetRecvHandler(&iic, &iic, (XIic_Handler) ReceiveHandler);
	XIic_SetStatusHandler(&iic, &iic, (XIic_StatusHandler) StatusHandler);

#ifdef XPAR_INTC_0_DEVICE_ID
	//AXI IIC的中断连接在AXI INTC器件上,sys_intr_inst()获取其实例
	XIntc_Connect(sys_intr_inst(), XPAR_INTC_0_IIC_0_VEC_ID, (XInterruptHandler) XIic_InterruptHandler, &iic);  //连接公共中断函数
	XIntc_Enable(sys_intr_inst(), XPAR_INTC_0_IIC_0_VEC_ID);
#else
	XScuGic_Connect(sys_intr_inst(), XPAR_FABRIC_AXI_IIC_IIC2INTC_IRPT_INTR, (Xil_InterruptHandler)XIic_InterruptHandler, &iic);
    XScuGic_Enable(sys_intr_inst(), XPAR_FABRIC_AXI_IIC_IIC2INTC_IRPT_INTR);
#endif

	return XST_SUCCESS;
}


static void SendHandler(XIic *InstancePtr)
{
	TransmitComplete = 0;
}

static void ReceiveHandler(XIic *InstancePtr)
{
	ReceiveComplete = 0;
}

static void StatusHandler(XIic *InstancePtr, int Event)
{

}

设置ov5640 I2C从设备的地址,注意ov5640的器件地址只有7位,最低位是读写标志位。

常用I2C接口通用器件的器件地址是由种类型号,及寻址码组成的,共7位。

格式为:D7 D6 D5 D4 D3 D2 D1 D0

1、器件类型:D7-D4 共4位决定的。这是由半导公司生产时就已固定此类型的了,也就是说这4位已是固定的。

2、用户自定义地址码:D3-D1共3位。这是由用户自己设置的,通常的作法如EEPROM这些器件是由外部IC的3个引脚所组合电平决定的(用常用的名字如A0,A1,A2)。这也就是寻址码。所以为什么同一IIC总线上同一型号的IC只能最多共挂8片同种类芯片的原因了。

3、最低一位就是R/W位,,“0”表示写,“1”表示读(通常读写信号中写上面有一横线,表示低电平)。所以I2C设备通常有两个地址,即读地址和写地址。

而OV5640厂家提供的0x78地址实际上就是上述D7~D0,因此实际上它的器件地址应该是D7~D1,最低位在具体读写的时候设置。

而有些厂家提供I2C器件地址是【0  D7~D1】即没带读写控制位,因此需要仔细阅读DataSheet

#define  OV5640_ID    0x78   //OV5640的ID
iic_set_slave_addr(OV5640_ID>>1); 

static int iic_set_slave_addr(u8 addr)
{
	int rc;
	rc = XIic_SetAddress(&iic, XII_ADDR_TO_SEND_TYPE, addr);
	if (rc != XST_SUCCESS) {
		xil_printf("XIic_SetAddress FAILURE\n");
		return rc;
	}
	return XST_SUCCESS;
}

OV5640内部寄存器是2字节位宽(16bit)的,因此需要封装好I2C读写16位寄存器的操作函数,源码如下所示

static volatile u8 TransmitComplete;
static volatile u8 ReceiveComplete;


static u8 sccb_read_reg16(u16 addr )
{
  	u8 TxBuffer[2] = {addr >> 8, addr & 0x00FF};
  	u8 RxBuffer[1] = {0};
	int Status = iic_write_data(TxBuffer, sizeof(TxBuffer));
	if (Status == XST_SUCCESS) {
		Status = iic_read_data(RxBuffer, sizeof(RxBuffer));
	}
	if (Status != XST_SUCCESS) {
		return 0;
	}
	return RxBuffer[0];
}


static int iic_write_data(const u8 *WriteBuffer, int len)
{
	int Status;
	unsigned timecnt = 1000000;

	TransmitComplete = 1;
	iic.Stats.TxErrors = 0;

	Status = XIic_Start(&iic);
	if (Status != XST_SUCCESS) {
		printf("iic_write_data XIic_Start fail %d\n", Status);
		return XST_FAILURE;
	}

	Status = XIic_MasterSend(&iic, (u8*)WriteBuffer, len);
	if (Status != XST_SUCCESS) {
		printf("iic_write_data XIic_MasterSend fail %d\n", Status);
		return XST_FAILURE;
	}

	while (((TransmitComplete) || (XIic_IsIicBusy(&iic) == TRUE)) && --timecnt) {
		usleep(1);
		//xil_printf(".");
	}
	if (TransmitComplete) {
		printf("iic_write_data timeout!!!\n");
	}

	Status = XIic_Stop(&iic);
	if (Status != XST_SUCCESS) {
		printf("iic_write_data XIic_Stop fail %d\n", Status);
		return XST_FAILURE;
	}

	return XST_SUCCESS;
}

static int iic_read_data(u8 *BufferPtr, int len)
{
	int Status;
	unsigned timecnt = 1000000;

	ReceiveComplete = 1;

	Status = XIic_Start(&iic);
	if (Status != XST_SUCCESS) {
		printf("iic_read_data XIic_Start fail %d\n", Status);
		return XST_FAILURE;
	}

	Status = XIic_MasterRecv(&iic, BufferPtr, len);
	if (Status != XST_SUCCESS) {
		printf("iic_read_data XIic_MasterRecv fail %d\n", Status);
		return XST_FAILURE;
	}

	while (((ReceiveComplete) || (XIic_IsIicBusy(&iic) == TRUE)) && --timecnt) {
		usleep(1);
		//xil_printf(".");
	}
	if (ReceiveComplete) {
		printf("iic_read_data timeout!!!\n");
	}

	Status = XIic_Stop(&iic);
	if (Status != XST_SUCCESS) {
		printf("iic_read_data XIic_Stop fail %d\n", Status);
		return XST_FAILURE;
	}

	return XST_SUCCESS;
}

后续即可通过sccb_read_reg16()和sccb_write_reg16()函数对OV5640内部寄存器进行读写操作。

ov5640_init()函数内容如下:

int ov5640_init(u16 cmos_h_pixel,  u16 cmos_v_pixel, u16* ptr_total_v_std)
{
	u16 cam_id = 0;

	usleep(20000);  //OV5640上电到开始配置sccb至少等待20ms

	sccb_init();    //初始化SCCB串行线(IIC的阉割版)

    //读OV5640摄像头ID
    cam_id  = sccb_read_reg16(0x300b);       //LSB  0x40
    cam_id |= sccb_read_reg16(0x300a) << 8;  //MSB  0x56
	//cam_id = 0x5640;
    
    if(cam_id != 0x5640) { //获取到正确的OV5640 ID
    	printf("cam_id Invalid %04X\r\n", cam_id);
    	return 1;
    }else{
		//先对寄存器进行软件复位,使寄存器恢复初始值
		//寄存器软件复位后,需要延时1ms才能配置其它寄存器
		sccb_write_reg16(0x3008,0x82); //Bit[7]:复位 Bit[6]:电源不休眠

		//延时1ms
		usleep(1000);

		//根据输出的格式选择不同的配置
		if (1280 == cmos_h_pixel && 960 == cmos_v_pixel) {
			ov5640_init_96mhz_raw_960p_45fps();
		}
		if (1920 == cmos_h_pixel && 1080 == cmos_v_pixel) {
			ov5640_init_96mhz_raw_1080p_30fps();
		}
		if (2592 == cmos_h_pixel && 1944 == cmos_v_pixel) {
			ov5640_init_96mhz_raw_5mp_15fps();
		}
		if (ptr_total_v_std) {
			*ptr_total_v_std = total_v_std;
		}
    return 0;
}

开源项目中配置的RAW输出尺寸为2592x1944,因此会选择对应的函数进行配置。

OV5640内部寄存器的详解参考往期博客:摄像头配置——OV5640配置输出RAW格式-CSDN博客

ISP及图像处理IP配置

这里主要配置以下几个IP核

配置的内容为:复位相关IP——配置IP的中断屏蔽位——Disable彩条显示——使能ISP Pipeline中的处理模块——配置gama LUT查找表参数——配置2DNR降噪参数——配置BLC——配置AE统计区域——配置两个xil_vip IP核——结束IP核复位——连接并使能相关中断——重新配置相关中断屏蔽位

代码如下:

static void init_camif_isp_vip()
{
	//复位相关IP
	XIL_CAMIF_mWriteReg(CAMIF_BASE, CAMIF_REG_RESET, 1);
	XIL_ISP_LITE_mWriteReg(ISP_BASE, ISP_REG_RESET, 1);
	XIL_VIP_mWriteReg(VIP_LCD_BASE, VIP_REG_RESET, 1);
	XIL_VIP_mWriteReg(VIP_DVI_BASE, VIP_REG_RESET, 1);

	//设置IP的中断屏蔽
	XIL_CAMIF_mWriteReg(CAMIF_BASE, CAMIF_REG_INT_MASK, 0xffff);
	XIL_ISP_LITE_mWriteReg(ISP_BASE, ISP_REG_INT_MASK, 0xffff);
	XIL_VIP_mWriteReg(VIP_LCD_BASE, VIP_REG_INT_MASK, 0xffff);
	XIL_VIP_mWriteReg(VIP_DVI_BASE, VIP_REG_INT_MASK, 0xffff);
	usleep(100000);
	
	//不使能彩条显示
	XIL_CAMIF_mWriteReg(CAMIF_BASE, CAMIF_REG_COLORBAR_EN, 0);
	
	//配置ISP Pipeline中使能的模块
	unsigned int isp_top_en = 0;
	isp_top_en |= ISP_REG_TOP_EN_BIT_DPC_EN;  
	isp_top_en |= ISP_REG_TOP_EN_BIT_BLC_EN;
	isp_top_en |= ISP_REG_TOP_EN_BIT_BNR_EN;
	isp_top_en |= ISP_REG_TOP_EN_BIT_DGAIN_EN;
	isp_top_en |= ISP_REG_TOP_EN_BIT_DEMOSIC_EN;
	isp_top_en |= ISP_REG_TOP_EN_BIT_WB_EN;
	isp_top_en |= ISP_REG_TOP_EN_BIT_CCM_EN;
	isp_top_en |= ISP_REG_TOP_EN_BIT_CSC_EN;
	isp_top_en |= ISP_REG_TOP_EN_BIT_GAMMA_EN;
	isp_top_en |= ISP_REG_TOP_EN_BIT_2DNR_EN;
	isp_top_en |= ISP_REG_TOP_EN_BIT_EE_EN;
	isp_top_en |= ISP_REG_TOP_EN_BIT_STAT_AE_EN;
	isp_top_en |= ISP_REG_TOP_EN_BIT_STAT_AWB_EN;
	XIL_ISP_LITE_mWriteReg(ISP_BASE, ISP_REG_TOP_EN, isp_top_en);   //使能ISP相关模块

	isp_init_gamma(ISP_BASE);    //配置gama变换的LUT
	isp_init_2dnr(ISP_BASE);     //配置2dNR相关参数
	XIL_ISP_LITE_mWriteReg(ISP_BASE, ISP_REG_BLC_R,  19<<(ISP_BITS-8));  //配置黑电平相关参数
	XIL_ISP_LITE_mWriteReg(ISP_BASE, ISP_REG_BLC_GR, 19<<(ISP_BITS-8));
	XIL_ISP_LITE_mWriteReg(ISP_BASE, ISP_REG_BLC_GB, 19<<(ISP_BITS-8));
	XIL_ISP_LITE_mWriteReg(ISP_BASE, ISP_REG_BLC_B,  19<<(ISP_BITS-8));

//	XIL_ISP_LITE_mWriteReg(ISP_BASE, ISP_REG_BLC_R,  32<<(ISP_BITS-8));  //配置黑电平相关参数
//	XIL_ISP_LITE_mWriteReg(ISP_BASE, ISP_REG_BLC_GR, 32<<(ISP_BITS-8));
//	XIL_ISP_LITE_mWriteReg(ISP_BASE, ISP_REG_BLC_GB, 32<<(ISP_BITS-8));
//	XIL_ISP_LITE_mWriteReg(ISP_BASE, ISP_REG_BLC_B,  32<<(ISP_BITS-8));

	XIL_ISP_LITE_mWriteReg(ISP_BASE, ISP_REG_NR_LEVEL, 2);     //配置降噪强度
	XIL_ISP_LITE_mWriteReg(ISP_BASE, ISP_REG_DGAIN_GAIN, 0x10);//1.0x   配置ISP Dgain参数

	XIL_ISP_LITE_mWriteReg(ISP_BASE, ISP_REG_STAT_AE_RECT_X, CAM_WIDTH/4);   //配置AE统计区域参数
	XIL_ISP_LITE_mWriteReg(ISP_BASE, ISP_REG_STAT_AE_RECT_Y, CAM_HEIGHT/4);
	XIL_ISP_LITE_mWriteReg(ISP_BASE, ISP_REG_STAT_AE_RECT_W, CAM_WIDTH/2);
	XIL_ISP_LITE_mWriteReg(ISP_BASE, ISP_REG_STAT_AE_RECT_H, CAM_HEIGHT/2);

	//LCD VIP
	unsigned int vip_top_en = 0;
	//这里直接相除用的不是浮点数?
	unsigned scale_h = CAM_WIDTH / LCD_WIDTH;
	unsigned scale_v = CAM_HEIGHT / LCD_HEIGHT;
	//vip_top_en |= VIP_REG_TOP_EN_BIT_HIST_EQU_EN;
	//vip_top_en |= VIP_REG_TOP_EN_BIT_SOBEL_EN;
	vip_top_en |= VIP_REG_TOP_EN_BIT_YUV2RGB_EN;  //YUV转RGB
	vip_top_en |= VIP_REG_TOP_EN_BIT_CROP_EN;     //使能裁剪功能
	vip_top_en |= VIP_REG_TOP_EN_BIT_OSD_EN;      //使能图层叠加
	if (scale_h > 1 && scale_v > 1) {
		vip_top_en |= VIP_REG_TOP_EN_BIT_DSCALE_EN; //若LCD显示尺寸小于ISP处理输出的图像尺寸说明需要缩小
	}
	XIL_VIP_mWriteReg(VIP_LCD_BASE, VIP_REG_TOP_EN, vip_top_en);

	vip_init_osd(VIP_LCD_BASE, 16, 16, 0xff0000, 0x888888);  //叠加显示的字符数据

	if (vip_top_en & VIP_REG_TOP_EN_BIT_DSCALE_EN) {         //裁剪以显示在LCD中
		unsigned scale_val = scale_h < scale_v ? scale_h : scale_v;  //最大化裁剪
		XIL_VIP_mWriteReg(VIP_LCD_BASE, VIP_REG_CROP_X, (CAM_WIDTH-LCD_WIDTH*scale_val)/2);
		XIL_VIP_mWriteReg(VIP_LCD_BASE, VIP_REG_CROP_Y, (CAM_HEIGHT-LCD_HEIGHT*scale_val)/2);
		XIL_VIP_mWriteReg(VIP_LCD_BASE, VIP_REG_CROP_W, LCD_WIDTH*scale_val);
		XIL_VIP_mWriteReg(VIP_LCD_BASE, VIP_REG_CROP_H, LCD_HEIGHT*scale_val);
		XIL_VIP_mWriteReg(VIP_LCD_BASE, VIP_REG_DSCALE_H, scale_val-1);
		XIL_VIP_mWriteReg(VIP_LCD_BASE, VIP_REG_DSCALE_V, scale_val-1);
	} else {
		XIL_VIP_mWriteReg(VIP_LCD_BASE, VIP_REG_CROP_X, (CAM_WIDTH-LCD_WIDTH)/2);
		XIL_VIP_mWriteReg(VIP_LCD_BASE, VIP_REG_CROP_Y, (CAM_HEIGHT-LCD_HEIGHT)/2);
		XIL_VIP_mWriteReg(VIP_LCD_BASE, VIP_REG_CROP_W, LCD_WIDTH);
		XIL_VIP_mWriteReg(VIP_LCD_BASE, VIP_REG_CROP_H, LCD_HEIGHT);
	}

	//DVI VIP
	vip_top_en = 0;
	scale_h = CAM_WIDTH / DVI_WIDTH;
	scale_v = CAM_HEIGHT / DVI_HEIGHT;
	//vip_top_en |= VIP_REG_TOP_EN_BIT_HIST_EQU_EN;
	//vip_top_en |= VIP_REG_TOP_EN_BIT_SOBEL_EN;
	//vip_top_en |= VIP_REG_TOP_EN_BIT_YUV444TO422_EN;
	vip_top_en |= VIP_REG_TOP_EN_BIT_YUV2RGB_EN;
	vip_top_en |= VIP_REG_TOP_EN_BIT_CROP_EN;
	vip_top_en |= VIP_REG_TOP_EN_BIT_OSD_EN;
	if (scale_h > 1 && scale_v > 1) {
		vip_top_en |= VIP_REG_TOP_EN_BIT_DSCALE_EN;
	}
	XIL_VIP_mWriteReg(VIP_DVI_BASE, VIP_REG_TOP_EN, vip_top_en);
	XIL_VIP_mWriteReg(VIP_DVI_BASE, VIP_REG_HIST_EQU_MIN, 20);
	XIL_VIP_mWriteReg(VIP_DVI_BASE, VIP_REG_HIST_EQU_MAX, 200);
	vip_init_osd(VIP_DVI_BASE, 16, 16, 0xffffff, 0x888888);

	if (vip_top_en & VIP_REG_TOP_EN_BIT_DSCALE_EN) {     //裁剪以显示在DVI中
		unsigned scale_val = scale_h < scale_v ? scale_h : scale_v;
		XIL_VIP_mWriteReg(VIP_DVI_BASE, VIP_REG_CROP_X, (CAM_WIDTH-DVI_WIDTH*scale_val)/2);
		XIL_VIP_mWriteReg(VIP_DVI_BASE, VIP_REG_CROP_Y, (CAM_HEIGHT-DVI_HEIGHT*scale_val)/2);
		XIL_VIP_mWriteReg(VIP_DVI_BASE, VIP_REG_CROP_W, DVI_WIDTH*scale_val);
		XIL_VIP_mWriteReg(VIP_DVI_BASE, VIP_REG_CROP_H, DVI_HEIGHT*scale_val);
		XIL_VIP_mWriteReg(VIP_DVI_BASE, VIP_REG_DSCALE_H, scale_val-1);
		XIL_VIP_mWriteReg(VIP_DVI_BASE, VIP_REG_DSCALE_V, scale_val-1);
	} else {
		XIL_VIP_mWriteReg(VIP_DVI_BASE, VIP_REG_CROP_X, (CAM_WIDTH-DVI_WIDTH)/2);
		XIL_VIP_mWriteReg(VIP_DVI_BASE, VIP_REG_CROP_Y, (CAM_HEIGHT-DVI_HEIGHT)/2);
		XIL_VIP_mWriteReg(VIP_DVI_BASE, VIP_REG_CROP_W, DVI_WIDTH);
		XIL_VIP_mWriteReg(VIP_DVI_BASE, VIP_REG_CROP_H, DVI_HEIGHT);
	}

	XIL_CAMIF_mWriteReg(CAMIF_BASE, CAMIF_REG_RESET, 0);  //复位结束
	XIL_ISP_LITE_mWriteReg(ISP_BASE, ISP_REG_RESET, 0);
	XIL_VIP_mWriteReg(VIP_LCD_BASE, VIP_REG_RESET, 0);
	XIL_VIP_mWriteReg(VIP_DVI_BASE, VIP_REG_RESET, 0);

	printf("vi_reset  = %08lX\n", XIL_CAMIF_mReadReg(CAMIF_BASE, CAMIF_REG_RESET));
	printf("isp_reset = %08lX\n", XIL_ISP_LITE_mReadReg(ISP_BASE, ISP_REG_RESET));
	printf("vip_reset = %08lX\n", XIL_VIP_mReadReg(VIP_LCD_BASE, VIP_REG_RESET));
	printf("vip_reset = %08lX\n", XIL_VIP_mReadReg(VIP_DVI_BASE, VIP_REG_RESET));

	camera_intr_init();   //使能并连接相关中断
	XIL_CAMIF_mWriteReg(CAMIF_BASE, CAMIF_REG_INT_MASK, ~CAMIF_REG_INT_MASK_BIT_FRAME_DONE);
	XIL_ISP_LITE_mWriteReg(ISP_BASE, ISP_REG_INT_MASK, ~ISP_REG_INT_MASK_BIT_FRAME_START);
	XIL_VIP_mWriteReg(VIP_LCD_BASE, VIP_REG_INT_MASK, ~VIP_REG_INT_MASK_BIT_FRAME_DONE);
	XIL_VIP_mWriteReg(VIP_DVI_BASE, VIP_REG_INT_MASK, ~VIP_REG_INT_MASK_BIT_FRAME_DONE);
}

配置VDMA IP核的帧缓存地址

该项目使用了3个VDMA IP核

第一个VDMA IP核用来解耦摄像头输出和ISP处理输入,这两个模块所提供的时钟一个是96MHz,另一个是120MHz因此帧率是不一样的,需要VDMA作为缓冲区来解耦二者(在实际设计中这里应该使用相同的时钟),另外一个功能是将RAW保存在DDR中可以在Vitis中通过挂载SD卡并将RAW数据保存下来,便于ISP算法的开发和离线ISP调试。

第二个VDMA IP用于用于解耦xil_vip处理输出和axis_to_video输入,以匹配不同的帧率,LCD输出的帧率为800x480@60 FPS,而xil_vip处理输出的帧率为21帧左右【120MHz/(2884x1968)=21帧】,使用VDMA可以匹配不同的帧率,

第三个VDMA IP也是用于解耦视频处理输出和显示输出通路的,DVI显示以1280x720@60FPS输出,而xil_vip处理输出的帧率为21帧,通过VDMA即可配置两种不同的帧率。

static int cam_buff = XPAR_PS7_DDR_0_S_AXI_BASEADDR+0x4000000; //RAW8
static int lcd_buff = XPAR_PS7_DDR_0_S_AXI_BASEADDR+0x5000000; //RGB888
static int dvi_buff = XPAR_PS7_DDR_0_S_AXI_BASEADDR+0x6000000; //RGB888

run_triple_frame_buffer(&cam_vdma_inst, XPAR_AXIVDMA_0_DEVICE_ID,CAM_WIDTH, CAM_HEIGHT, cam_buff, 0, 0, BOTH);    //RAW
run_triple_frame_buffer(&lcd_vdma_inst, XPAR_AXIVDMA_1_DEVICE_ID,LCD_WIDTH, LCD_HEIGHT, lcd_buff, 0, 0, BOTH);    //LCD
run_triple_frame_buffer(&dvi_vdma_inst, XPAR_AXIVDMA_2_DEVICE_ID,DVI_WIDTH, DVI_HEIGHT, dvi_buff, 0, 0, BOTH);

run_triple_frame_buffer()函数是三帧缓存VDMA读写驱动函数,配置好后VDMA会自动开始传输帧缓冲中的图像帧数据。

while循环

主函数的配置部分基本完成,接下来是while循环,在while循环中每隔1s计算一次自动曝光和自动白平衡值。

自动曝光(Auto Exposure,AE)

自动曝光是一个反馈控制系统,需要不断判断当前图像亮度是否为合适的亮度,并不断使图像亮度接近计算出来的最合适亮度,图像亮度需要控制摄像头的AEC相关寄存器和AGC相关寄存器,即亮度由曝光量(Exposure Time)和增益()决定

曝光量设置

OV5640的快门控制着曝光时间。快门的单位是行周期。快门值对每个输出分辨率都有限制。ov5640参考手册中手动配置曝光量的解释如下:

要手动更改曝光值,必须首先同时设置0x3503[0],其中0x3503[0]将启用手动曝光控制。在自动曝光模式下,寄存器0x350C/0x350D中的额外曝光值(大于1帧)会自动发生变化。在手动曝光模式下,这些寄存器将不会自动更改。在寄存器0x3500~0x3502中手动设置的曝光必须小于{0x380E,0x380F} + {0x350C,0x350D}中的最大曝光值。寄存器0x3500~0x3502中的曝光值以行*16为单位,低4位(0x3502[3:0])是行的分数位,{0x380E + 0x380F} + {0x350C,0x350D}中的最大值以行为单位。如果手动设置的曝光值小于一个预定义的帧周期(例如,15帧/秒中的1/15秒),则无需更改0x380E/0x380F。如果曝光值需要设定超过预定的帧周期,换句话说,如果帧周期需要延长以延长曝光时间,则需要先设定0x380E/0x380F的最大帧值,则曝光可以相应设置为寄存器0x3500~0x3502

因此曝光量的设置如下:

int cmos_set_exposure(unsigned exposure)
{
	static unsigned int cmos_exposure = 0x300;
	if (exposure == cmos_exposure) {
		return 0;
	}
	//printf("cmos_exposure %u -> %u\n", cmos_exposure, exposure);
	cmos_exposure = exposure;
    sccb_write_reg16(0x3500, (exposure>>12)&0x0ff);// Exposure [19:16]
	sccb_write_reg16(0x3501, (exposure>>4)&0x0ff);// Exposure [15:8]
	sccb_write_reg16(0x3502, (exposure&0x0f)<<4);// Exposure [7:0]

	if (exposure < total_v_std) {
		sccb_write_reg16(0x380e, total_v_std>>8);    //垂直总像素大小高5位
		sccb_write_reg16(0x380f, total_v_std&0xff);  //垂直总像素大小低8位
	} else {
		sccb_write_reg16(0x380e, exposure>>8);    //垂直总像素大小高5位
		sccb_write_reg16(0x380f, exposure&0xff);  //垂直总像素大小低8位
	}
	return 0;
}
增益设置

要手动更改增益,首先设置寄存器位0x3503[1]以启用手动控制,然后更改针对手动增益的0x350A/0x350B中的值,OV5640的最大增益为64倍。、

设置增益代码如下:

int cmos_set_gain(unsigned gain)
{
	static unsigned int cmos_gain = 0xa0;
	if (gain == cmos_gain) {
		return 0;
	}
	//printf("cmos_gain 0x%03X -> 0x%03X\n", cmos_gain, gain);
	cmos_gain = gain;
	sccb_write_reg16(0x350a, (gain>>8)&0x3);// Real gain[9:8]
	sccb_write_reg16(0x350b, gain&0x0ff);// Real gain[7:0]
	return 0;
}

因此通过调整曝光量和增益就能实现对自动曝光的设置,自动曝光的反馈控制如下:

void isp_ae_handler(IspContext *context)
{
	UINTPTR base = context->base;
	unsigned cur_exposure = context->ae_cur_exposure;
	unsigned cur_gain = context->ae_cur_gain;
	unsigned cur_isp_dgain = context->ae_cur_isp_dgain;
	unsigned tar_luma = context->ae_target_luminance;
	unsigned max_exposure = context->ae_max_exposure;
	unsigned max_gain = context->ae_max_gain;

	unsigned long long pix_cnt = XIL_ISP_LITE_mReadReg(base, ISP_REG_STAT_AE_PIX_CNT_L) | ((unsigned long long)XIL_ISP_LITE_mReadReg(base, ISP_REG_STAT_AE_PIX_CNT_H) << 32);
	unsigned long long luma_sum = XIL_ISP_LITE_mReadReg(base, ISP_REG_STAT_AE_SUM_L) | ((unsigned long long)XIL_ISP_LITE_mReadReg(base, ISP_REG_STAT_AE_SUM_H) << 32);
	unsigned tar_factor = (pix_cnt * tar_luma * 16 + luma_sum / 2) / luma_sum;
	tar_factor = tar_factor > 2*16 ? 2*16 : tar_factor;

	unsigned tar_exposure = cur_exposure;
	unsigned tar_gain = cur_gain;
	unsigned tar_isp_dgain = cur_isp_dgain;
	unsigned expo_diff, gain_diff, isp_dgain_diff;
	if (tar_factor > 18) {
		expo_diff = (((cur_exposure * tar_factor) >> 4) - cur_exposure) >> 1;
		expo_diff = expo_diff > 0 ? expo_diff : 1;
		gain_diff = (((cur_gain * tar_factor) >> 4) - cur_gain) >> 1;
		gain_diff = gain_diff > 0 ? gain_diff : 1;
		isp_dgain_diff = (((cur_isp_dgain * tar_factor) >> 4) - cur_isp_dgain) >> 1;
		isp_dgain_diff = isp_dgain_diff > 0 ? isp_dgain_diff : 1;
		if (cur_exposure < max_exposure) {
			if (cur_exposure + expo_diff > max_exposure)
				tar_exposure = max_exposure;
			else
				tar_exposure = cur_exposure + expo_diff;
		}
		else if (cur_gain < max_gain) {
			if (cur_gain + gain_diff > max_gain)
				tar_gain = max_gain;
			else
				tar_gain = cur_gain + gain_diff;
		}
		else if (cur_isp_dgain < 0x0ff) {
			if (cur_isp_dgain + isp_dgain_diff > 0x0ff)
				tar_isp_dgain = 0xff;
			else
				tar_isp_dgain = cur_isp_dgain + isp_dgain_diff;
		}
	}
	else if (tar_factor < 14) {
		expo_diff = (cur_exposure - ((cur_exposure * tar_factor) >> 4)) >> 1;
		expo_diff = expo_diff > 0 ? expo_diff : 1;
		gain_diff = (cur_gain - ((cur_gain * tar_factor) >> 4)) >> 1;
		gain_diff = gain_diff > 0 ? gain_diff : 1;
		isp_dgain_diff = (cur_isp_dgain - ((cur_isp_dgain * tar_factor) >> 4)) >> 1;
		isp_dgain_diff = isp_dgain_diff > 0 ? isp_dgain_diff : 1;
		if (cur_isp_dgain > 16) {
			if (cur_isp_dgain < 16 + isp_dgain_diff)
				tar_isp_dgain = 16;
			else
				tar_isp_dgain = cur_isp_dgain - isp_dgain_diff;
		}
		else if (cur_gain > 16) {
			if (cur_gain < 16 + gain_diff)
				tar_gain = 16;
			else
				tar_gain = cur_gain - gain_diff;
		}
		else if (cur_exposure > 1) {
			if (cur_exposure < 1 + expo_diff)
				tar_exposure = 1;
			else
				tar_exposure = cur_exposure - expo_diff;
		}
	}
	if (cur_exposure != tar_exposure || cur_gain != tar_gain || cur_isp_dgain != tar_isp_dgain) {
		printf("ALG_AEC: Exposure:%u Gain:0x%04X ISP_DGain:0x%02X (pixcnt:%llu sum_luma:%llu, avg_luma:%llu, target_luma:%u)\n",
				tar_exposure, tar_gain, tar_isp_dgain, pix_cnt, luma_sum, (luma_sum+pix_cnt/2)/pix_cnt, tar_luma);
		if (cur_exposure != tar_exposure)
			context->pfn_set_exposure(tar_exposure, context->priv_data);
		if (cur_gain != tar_gain)
			context->pfn_set_gain(tar_gain, context->priv_data);
		if (cur_isp_dgain != tar_isp_dgain)
			XIL_ISP_LITE_mWriteReg(base, ISP_REG_DGAIN_GAIN, tar_isp_dgain);
		context->ae_cur_exposure = tar_exposure;
		context->ae_cur_gain = tar_gain;
		context->ae_cur_isp_dgain = tar_isp_dgain;
	}
}

在主函数中,为isp_contex结构体中的函数指针赋值,然后在isp_ae_handler中即可实现对Ov5640的曝光量和增益进行手动设置。

	isp_context.pfn_set_exposure = _set_exposure;
	isp_context.pfn_set_gain = _set_gain;

自动白平衡(Auto White Balance,AWB)

自动白平衡调整R通道和B通道的增益使得图像颜色在不同光照下都具有颜色恒常性。ISP内部设置了AWB增益的寄存器,通过AXI-Lite向ISP IP核写入R和B通道的增益即可实现自动白平衡,增益的计算也是在嵌入式中进行。

void isp_awb_handler(IspContext *context)
{
	UINTPTR base = context->base;
	unsigned cur_rgain = context->awb_cur_rgain;
	unsigned cur_bgain = context->awb_cur_bgain;

	unsigned long long pix_cnt = XIL_ISP_LITE_mReadReg(base, ISP_REG_STAT_AWB_PIX_CNT_L) | ((unsigned long long)XIL_ISP_LITE_mReadReg(base, ISP_REG_STAT_AWB_PIX_CNT_H) << 32);
	unsigned long long sum_r = XIL_ISP_LITE_mReadReg(base, ISP_REG_STAT_AWB_SUM_R_L) | ((unsigned long long)XIL_ISP_LITE_mReadReg(base, ISP_REG_STAT_AWB_SUM_R_H) << 32);
	unsigned long long sum_g = XIL_ISP_LITE_mReadReg(base, ISP_REG_STAT_AWB_SUM_G_L) | ((unsigned long long)XIL_ISP_LITE_mReadReg(base, ISP_REG_STAT_AWB_SUM_G_H) << 32);
	unsigned long long sum_b = XIL_ISP_LITE_mReadReg(base, ISP_REG_STAT_AWB_SUM_B_L) | ((unsigned long long)XIL_ISP_LITE_mReadReg(base, ISP_REG_STAT_AWB_SUM_B_H) << 32);

	unsigned tar_rgain = ((sum_g << 4) + sum_r / 2) / sum_r;
	unsigned tar_bgain = ((sum_g << 4) + sum_b / 2) / sum_b;

	if (cur_rgain < tar_rgain) {
		if (tar_rgain - cur_rgain >= 10) {
			tar_rgain = cur_rgain + (tar_rgain - cur_rgain) / 5;
		} else {
			tar_rgain = cur_rgain + 1;
		}
	} else if (cur_rgain > tar_rgain) {
		if (cur_rgain - tar_rgain >= 10) {
			tar_rgain = cur_rgain - (cur_rgain - tar_rgain) / 5;
		} else {
			tar_rgain = cur_rgain - 1;
		}
	}
	if (cur_bgain < tar_bgain) {
		if (tar_bgain - cur_bgain >= 10) {
			tar_bgain = cur_bgain + (tar_bgain - cur_bgain) / 5;
		} else {
			tar_bgain = cur_bgain + 1;
		}
	} else if (cur_bgain > tar_bgain) {
		if (cur_bgain - tar_bgain >= 10) {
			tar_bgain = cur_bgain - (cur_bgain - tar_bgain) / 5;
		} else {
			tar_bgain = cur_bgain - 1;
		}
	}

	if (cur_rgain != tar_rgain || cur_bgain != tar_bgain) {
		printf("ALG_AWB RGain:0x%02X BGain:0x%02X (pixcnt:%llu sum_rgb:%llu,%llu,%llu, avg_rgb:%llu,%llu,%llu)\n",
				tar_rgain, tar_bgain, pix_cnt, sum_r, sum_g, sum_b, sum_r/pix_cnt, sum_g/pix_cnt, sum_b/pix_cnt);
		XIL_ISP_LITE_mWriteReg(base, ISP_REG_WB_GGAIN, 0x10);
		XIL_ISP_LITE_mWriteReg(base, ISP_REG_WB_RGAIN, tar_rgain);
		XIL_ISP_LITE_mWriteReg(base, ISP_REG_WB_BGAIN, tar_bgain);
		context->awb_cur_rgain = tar_rgain;
		context->awb_cur_bgain = tar_bgain;
	}
}

3A算法在后续章节会进行补充。

按键控制

项目中按键控制是通过在while循环中轮询控制的,各个按键的功能如下:

PL_RESET:复位相关IP,重置视频处理Pipeline

PL_KEY0:保存原始二进制RAW图到SD卡中

PL_KEY1:保存DVI通路中二进制RGB图到SD卡中

PS_KEY0:未定义功能的按键

PS_KEY1:使能xil_camif IP 核中的彩条显示,按下后会将数据通路切换未彩条。

static void key_control()
{
	{
		static int reset = 0;
		if (reset != !XGpioPs_ReadPin(&gpiops_inst, PL_RESET)) {           //PL RESET按键
			reset = !XGpioPs_ReadPin(&gpiops_inst, PL_RESET);
			printf("reset = %d\n", reset);
			XIL_CAMIF_mWriteReg(CAMIF_BASE, CAMIF_REG_RESET, reset);
			XIL_ISP_LITE_mWriteReg(ISP_BASE, ISP_REG_RESET, reset);
			XIL_VIP_mWriteReg(VIP_LCD_BASE, VIP_REG_RESET, reset);
			XIL_VIP_mWriteReg(VIP_DVI_BASE, VIP_REG_RESET, reset);
		}
	}
	if (fs_init && 0 == XGpioPs_ReadPin(&gpiops_inst, PL_KEY0)) {          //PL KEY0:保存RAW图数据
		printf("writing dump_cam_raw.raw ... ");
		Xil_DCacheInvalidateRange(cam_buff, CAM_WIDTH * CAM_HEIGHT);
		sd_write_data("dump_cam_raw.raw", cam_buff, CAM_WIDTH * CAM_HEIGHT);
		printf("done\n");
	}
	if (fs_init && 0 == XGpioPs_ReadPin(&gpiops_inst, PL_KEY1)) {         //PL KEY1:保存RGB数据
		printf("writing dump_dvi_rgb888.rgb ... ");
		Xil_DCacheInvalidateRange(dvi_buff, DVI_WIDTH * DVI_HEIGHT * 3);
		sd_write_data("dump_dvi_rgb888.rgb", dvi_buff, DVI_WIDTH * DVI_HEIGHT * 3);
		printf("done\n");
	}
	if (fs_init && 0 == XGpioPs_ReadPin(&gpiops_inst, PS_KEY0)) {        //PS KEY0

	}
	{
		static int colorbar_en = 0;
		if (colorbar_en != !XGpioPs_ReadPin(&gpiops_inst, PS_KEY1)) {    //彩条显示
			colorbar_en = !XGpioPs_ReadPin(&gpiops_inst, PS_KEY1);
			printf("colorbar_en = %d\n", colorbar_en);
			XIL_CAMIF_mWriteReg(CAMIF_BASE, CAMIF_REG_COLORBAR_EN, colorbar_en);
		}
	}
}

开源ISP 嵌入式Vits端的搭建讲解结束,以上仅是跑通开源ISP,后续章节会对具体IP 和算法的硬件实现进行讲解,并改进相关算法。

基于zynq7020最终实现的ISP效果如下所示

 


网站公告

今日签到

点亮在社区的每一天
去签到