软件版本:VIVADO2021.1
操作系统:WIN10 64bit
硬件平台:适用 XILINX A7/K7/Z7/ZU/KU 系列 FPGA
实验平台:米联客-MLK-H3-CZ08-7100开发板
板卡获取平台:https://milianke.tmall.com/
登录“米联客”FPGA社区 http://www.uisrc.com 视频课程、答疑解惑!
1概述
本实验通过 uifdma_dbuf + uifdma IP 实现 PL 到 PS 的数据通路部分,把摄像头数据传输到 PS DDR. PS 的 ARM部分通过 lwip tcp 方式把 DDR 中数据发送到上位机,并且显示。
实验目的:
1:掌握 uifdmadbuf 配置成视频模式的情况下的参数设置
2:掌握 OV5640 摄像头数据如何通过 uifdmadbuf 写入到 uifdma ip
3:ps 实现 uifdmadbuf 中断到来,读取 DDR 缓存中的数据
4:设计基于 TCP 方式的摄像头传输方案
5:使用 lwip tcp 方式把采集的摄像头数据从 DDR 中发送出去
6:可以通过上位机获取开发板发过来的相机数据,并且显示图像
2系统构架
本方案中继续保留之前 HDMI 输出的部分,这部分功能没有用到,用户可以自己裁剪掉该功能。
3硬件电路分析本方案使用到了FEP-GPIO-CEPX3模块,以及OV5640摄像头,模块的介绍请阅读“附录1” 4搭建SOC系统工程
4.1PL图形化编程
上图中高亮部是FDMA的帧同步计数器,CAM0的帧同步计数器提供给CAM1和VDMA 输出 IP使用。单VDMA 输出 IP工作于SLAVE circle 模式,所以CAM1和VDMA 输出都是从模式同步受控于CAM0的帧计数器,这样可以确保视频不出现撕裂。(本方案中可以优化掉VDMA部分,因为不需要显示功能) 另外,我们对CAM0和CAM1的IP在BD图像模块中做了层级封装,这样可以让复杂的BD图像设计,看起来更加简洁,如下图所示。
这个技巧如下,选中需要层级封装的IP,右击选择Create Hierarchy
1:CAM0中FDMA IP设置关键设置: 1`WBaseaddr 0x10000000 设置缓存的起始地址 2`WDsizebits 设置缓存的大小,2^23次方代表8MB大小 3`WBufsize 设置图像采用三缓存 4`WXsize设置行像素640 5`WXStride设置行Stride参数为1280,改参数用于2个视频在一个显存中显示 6`WYsize设置场像素为480
2:CAM1中FDMA IP设置1`WBaseaddr 0x1012CA00 设置缓存的起始地址,通过设置该地址可以让图像在指定的偏移位置显示 2`WDsizebits 设置缓存的大小,2^23次方代表8MB大小 3`WBufsize 设置图像采用三缓存 4`WXsize设置行像素640 5`WXStride设置行Stride参数为1280,改参数用于2个视频在一个显存中显示 6`WYsize设置场像素为480
3:视频输出 VDMA IP 设置
以下设置视频输出 VDMA 为从模式,帧同步跟随主模式的 CAM0/uifdma_dbuf (本方案中可以优化掉 VDMA 部分,因为不需要显示功能)
4:多路视频同屏显示原理
为了把2个图像显示到1个显示器,首先得搞清楚以下关系: hsize:每1行图像实际在内存中占用的有效空间,以32bit表示一个像素的时候占用内存大小为hsize*4 hstride:用于设置每行图像第一个像素的地址,以32bit 表示一个像素的时候h_cnt* hstride*4 vsize:有效的行 因此很容易得出cam0的每行第一个像素的地址也是h_cnt* hstride*4 同理如果我们需要把cam1在hsize和vsize空间的任何位置显示,我们只要关心cam1每一行图像第一个像素的地址,可以用以下公式h_cnt* hstride*4+offset 比如我们这里背景输出到显示器的分辨率为1280*720,cam1的分辨率是640*480需要移动上图的右下脚,offset=(1280-640)*4*(720-480) 4.2设置地址分配以sccb方式初始化摄像头的地址空间截图
4.3添加PIN约束1:选中PROJECT MANAGERà Add SourcesàAdd or create constraints,添加XDC约束文件。
2:打开提供例程,复制约束文件中的管脚约束到XDC文件,或者查看原理图,自行添加管脚约束,并保存。 以下是添加配套工程路径下已经提供的pin脚文件。配套工程的pin脚约束文件在uisrc/04_pin路径 4.4编译并导出平台文件1:单击Block文件à右键àGenerate the Output ProductsàGlobalàGenerate。 2:单击Block文件à右键à Create a HDL wrapper(生成HDL顶层文件)àLet vivado manager wrapper and auto-update(自动更新)。 3:生成Bit文件。 4:导出到硬件: FileàExport HardwareàInclude bitstream 5:导出完成后,对应工程路径的soc_hw路径下有硬件平台文件:system_wrapper.xsa的文件。根据硬件平台文件system_wrapper.xsa来创建需要Platform平台。
5搭建Vitis-sdk工程创建soc_base sdk platform和APP工程的过程不再重复,如果不清楚请参考本章节第一个demo。 5.1创建SDK Platform工程
LWIP库的修改: 1:新版本系列工业级开发板板载网口芯片是RTL8211FDI,由于默认的驱动不支持,需要手动自己修改库文件。我们这里已经提供了修改好的库,解压到vivado的安装路径下的对于路径下:
修改好后,需要关闭vitis-sdk然后重新打开sdk,否则无法识别修改的库
2:为了创建lwip工程需要先对zu_base中的board support package简称bsp设置lwip库的支持
3:对lwip库参数修改以达到最佳性能。
本例程使用 RAW API,即函数调用不依赖操作系统。传输效率也比 SOCKET API 高,(具体可参考 xapp1026)。 将 use_axieth_on_zynq 和 use_emaclite_on_zynq 设为 0。如下图所示。
修改 lwip_memory_options 设置,将 mem_size,memp_n_pbuf,mem_n_tcp_pcb,memp_n_tcp_seg 这 4 个参数 值设大,这样会提高 TCP 传输效率。如下图所示。
修改 pbuf_options 设置,将 pbuf_pool_size 设大,增加可用的 pbuf 数量,这样同样会提高 TCP 传输效率。如下 图所示。
修改 tcp_options 设置,将 tcp_snd_buf,tcp_wnd 参数设大,这样同样会提高 TCP 传输效率。如下图所示。
修改 temac_adapter_options 设置,将 n_rx_descriptors 和 n_tx_descriptors 参数设大。这样可以提高 zynq 内部 emac dma 的数据迁移效率,同样能提高 TCP 传输效率。如下图所示。
启用DHCP功能
修改完成后重新编译soc_base 5.2创建cam_lwip_tcp工程
6SDK程序分析
6.1FDMA数据接收原理
每当uifdmadbuf发送的中断后,该函数被调用,通过读取uifdmadbuf axi-lite的寄存器,获取当前哪一个缓存产生了中断(代表数据发送到PS DDR了)。 为了增加数据的吞吐能力,在中断中,不宜进行数据搬运,我们设计了要给结构体,可以用于标记已经写入DDR的数据。 1:PS_RX_intr_Handler
void PS_RX_intr_Handler(void *param)
{
fdma_buf.record[fdma_buf.circle_cnt]= Xil_In32((UINTPTR)FDMA_DBUF_BASE_ADDR);
if(fdma_buf.circle_cnt<2)
fdma_buf.circle_cnt ++ ;
else
fdma_buf.circle_cnt = 0;
fdma_buf.pkg_done_cnt++;
}
| }6.2数据包设计Lwip ip作为轻量级的协议栈,不能一次性发送所有的图像数据,因此需要对图像数据分多次传输。本文中,传输的图像大小为1280*720*4 = 3600KB,数据设计为每包传输1024*16即16KB。因此传输完所有数据需要经过225次。
#defineTCP_PACKEG_SIZE 1024*16
#define IMG_SIZE1280*720*4
#define TCP_SEND_TIMES IMG_SIZE/TCP_PACKEG_SIZE
#define TCP_SEND_LAST_SIZE IMG_SIZE-(TCP_PACKEG_SIZE*TCP_SEND_TIMES)
#define TCP_FIRST_SEND_SIZE HEADER_SIZE + TCP_PACKEG_SIZE
| 6.3帧头设计为了让上位机知道接收的数据的格式、大小、当前帧号、当前包号,设计了如下数据帧头:
typedef struct packet_header
{
u32 ID0;//AA55AA55
u32 ID1;//AA55AA55
u16 framcnt;//1,2
u16 hsize;//width
u16 vsize;//height
u16 offset;//
u32 psize; //this packet saize
u32 tsize; //total packet saize
}packet_header;
|
6.4主程序分析main函数中完成中断资源的初始化,lwip的初始化,并且通过一个while循环完成, tcp连接监听、数据的接收函数调用、数据的发送函数调用。本文的实验只需要,tcp连接监听和数据的发送功能。 通过定时器,每间隔250ms会判断一次request_pcb->state的状态,如果以太网没有连接,则会创建一个新的TCP连接。 1:init_intr_sys()该函数初始化中断,包括PL中断和以太网传输需要用到的定时器中断
其中init_platform函数会对以太网定时器中断部分以及回调函数进行初始化。 可以关键看下platform_zynq.c中被调用的相关函数:
void
timer_callback(XScuTimer * TimerInstance)
{
/* we need to call tcp_fasttmr & tcp_slowtmr at intervals specified
* by lwIP. It is not important that the timing is absoluetly accurate.
*/
static int odd = 1;
#if LWIP_DHCP==1
static int dhcp_timer = 0;
#endif
TcpFastTmrFlag = 1;
odd = !odd;
#ifndef USE_SOFTETH_ON_ZYNQ
ResetRxCntr++;
#endif
if (odd) {
TcpSlowTmrFlag = 1;
#if LWIP_DHCP==1
dhcp_timer++;
dhcp_timoutcntr--;
dhcp_fine_tmr();
if (dhcp_timer >= 120) {
dhcp_coarse_tmr();
dhcp_timer = 0;
}
#endif
}
/* For providing an SW alternative for the SI #692601. Under heavy
* Rx traffic if at some point the Rx path becomes unresponsive, the
* following API call will ensures a SW reset of the Rx path. The
* API xemacpsif_resetrx_on_no_rxdata is called every 100 milliseconds.
* This ensures that if the above HW bug is hit, in the worst case,
* the Rx path cannot become unresponsive for more than 100
* milliseconds.
*/
#ifndef USE_SOFTETH_ON_ZYNQ
if (ResetRxCntr >= RESET_RX_CNTR_LIMIT) {
xemacpsif_resetrx_on_no_rxdata(&server_netif);
ResetRxCntr = 0;
}
#endif
XScuTimer_ClearInterruptStatus(TimerInstance);
}
void platform_setup_timer(void)
{
int Status = XST_SUCCESS;
XScuTimer_Config *ConfigPtr;
int TimerLoadValue = 0;
ConfigPtr = XScuTimer_LookupConfig(TIMER_DEVICE_ID);
Status = XScuTimer_CfgInitialize(&TimerInstance, ConfigPtr,
ConfigPtr->BaseAddr);
if (Status != XST_SUCCESS) {
xil_printf("In %s: Scutimer Cfg initialization failed...\r\n",
__func__);
return;
}
Status = XScuTimer_SelfTest(&TimerInstance);
if (Status != XST_SUCCESS) {
xil_printf("In %s: Scutimer Self test failed...\r\n",
__func__);
return;
}
XScuTimer_EnableAutoReload(&TimerInstance);
/*
* Set for 250 milli seconds timeout.
*/
TimerLoadValue = XPAR_CPU_CORTEXA9_0_CPU_CLK_FREQ_HZ / 8;
XScuTimer_LoadTimer(&TimerInstance, TimerLoadValue);
return;
}
void platform_setup_interrupts(void)
{
/*
* Connect the device driver handler that will be called when an
* interrupt for the device occurs, the handler defined above performs
* the specific interrupt processing for the device.
*/
XScuGic_RegisterHandler(INTC_BASE_ADDR, TIMER_IRPT_INTR,
(Xil_ExceptionHandler)timer_callback,
(void *)&TimerInstance);
/*
* Enable the interrupt for scu timer.
*/
XScuGic_EnableIntr(INTC_DIST_BASE_ADDR, TIMER_IRPT_INTR);
return;
}
void platform_enable_interrupts()
{
/*
* Enable non-critical exceptions.
*/
Xil_ExceptionEnableMask(XIL_EXCEPTION_IRQ);
XScuTimer_EnableInterrupt(&TimerInstance);
XScuTimer_Start(&TimerInstance);
return;
}
void init_platform()
{
platform_setup_timer();
platform_setup_interrupts();
return;
}
|
2:lwip_init()初始化lwip 3:xemac_add ()添加以太网的MAC地址,MAC地址定义如下: unsigned char mac_ethernet_address[] ={0x00,0x0a,0x35,0x00,0x01,0x02}; 4:netif_set_default()设置默认的以太网接口,这里定义了一个server_netif的全局变量,并且对其初始化。 struct netif server_netif; netif = &server_netif; netif_set_default(netif); 5:platform_enable_interrupts()该函数会使能platform_zynq.c中的定时器,启动定时器,这样每间隔250msTcpFastTmrFlag变量就会设设置1,每间隔500ms TcpSlowTmrFlag变量就会设置1 6:启动DHCP配置
dhcp_start(netif);
dhcp_timoutcntr = 24;
while (((netif->ip_addr.addr) == 0) && (dhcp_timoutcntr > 0))
xemacif_input(netif);
if (dhcp_timoutcntr <= 0) {
if ((netif->ip_addr.addr) == 0) {
xil_printf("ERROR: DHCP request timed out\r\n");
assign_default_ip(&(netif->ip_addr),
&(netif->netmask), &(netif->gw));
}
}
| 7:start_appication()
该函数首先
void start_application(void)
{
err_t err;
ip_addr_t remote_addr;
u32_t i;
cam_init();/* 初始化摄像头 */
first_trans_start = 0; /* 该变量判断是否第一次传输 */
client_connected =0; /* 判断是否连接状态 */
#if LWIP_IPV6==1
remote_addr.type= IPADDR_TYPE_V6;
err = inet6_aton(TCP_SERVER_IPV6_ADDRESS, &remote_addr);
#else
err = inet_aton(TCP_SERVER_IP_ADDRESS, &remote_addr); /* 设置服务器IP地址 */
#endif /* LWIP_IPV6 */
if (!err) {
xil_printf("Invalid Server IP address: %d\r\n", err);
return;
}
/* Create Client PCB */
request_pcb = tcp_new_ip_type(IPADDR_TYPE_ANY); /* 创建一个客户端PCB */
if (!request_pcb) {
xil_printf("Error in PCB creation. out of memory\r\n");
return;
}
err = tcp_connect(request_pcb, &remote_addr, TCP_CONN_PORT,
tcp_client_connected); /* 设置客户端和主机连接上的回调函数 tcp_client_connected */
if (err) {
xil_printf("Error on tcp_connect: %d\r\n", err);
tcp_client_close(request_pcb);
return;
}
client.client_id = 0;
return;
}
| 8:while循环
该循环中,每过250ms调用tcp_fasttmr(),每间隔500ms调用tcp_slowTmr()函数。tcp_fasttmr()每250ms处理延时发送的ack报文和fin报文,并且通知上层应用处理数据。tcp_slowTmr()每500ms调用,该函数负责超时重传以及移除TIME-WAIT 足够时间的 PCB,同时将PCB中unsent队列中的数据发送出去。一般使用tcp_write();写入数据后,数据不会马上发送,而是在定时任务中发送。
While循环中还会检测当前连接状体,如果当前连接状态不存在会每间隔250ms重新尝试连接一次。
最后当连接建立后,会调用transfer_data()完成数据从DDR到以太网的发送。
while (1) {
if (TcpFastTmrFlag) {
if(request_pcb->state == CLOSED || (request_pcb->state == SYN_SENT && request_pcb->nrtx ==
TCP_SYNMAXRTX))//check conditions for create new tcp connection
{
start_application();
}
tcp_fasttmr();
TcpFastTmrFlag = 0;
}
if (TcpSlowTmrFlag) {
tcp_slowtmr();
TcpSlowTmrFlag = 0;
}
xemacif_input(netif);
if(client_connected)
transfer_data();
}
| 8:transfer_data()该函数负责把DDR中摄像头的图像数据发送出去,是本方案的核心。该函数会调用tcp_send_perf_traffic()函数。 9:tcp_send_perf_traffic()函数当FDMA摄像头的缓存中存在数据,首选发送帧头,然后连续发送TCP_SEND_TIMES次TCP_PACKEG_SIZE大小的数据包,直到所有数据完成发送。 static err_t tcp_send_perf_traffic(void)
{
err_t err;
u8_t apiflags = TCP_WRITE_FLAG_COPY | TCP_WRITE_FLAG_MORE;
if (c_pcb == NULL) {
return ERR_CONN;
}
#ifdef __MICROBLAZE__
/* Zero-copy pbufs is used to get maximum performance for Microblaze.
* For Zynq A9, ZynqMP A53 and R5 zero-copy pbufs does not give
* significant improvement hense not used. */
apiflags = 0;
#endif
struct tcp_pcb *tpcb = c_pcb;
if (!tpcb)
return;
if(first_trans_start==0)
{
first_trans_start =1;
fdma_buf.circle_cnt=0;
fdma_buf.next=0;
fdma_buf.pkg_done_cnt=0;
fdma_buf.pkg_cnt=0;
fdma_buf.fram_cnt=0;
pkg_psize =0;
pkg_offset =0;
XGpio_DiscreteWrite(&rstn_5640, 1, 0x1);
}
/*当fdma_buf.pkg_done_cnt 大于0标识数据已经从PL发送到DDR,当fdma_buf.pkg_done_cnt 大于2标识缓存溢出*/
if(fdma_buf.pkg_done_cnt> 0 && fdma_buf.pkg_done_cnt<3) //1MB divide in to 64 times
{
/*当TCP发送缓冲区足够发送一包数据,则发送数据*/
if (tcp_sndbuf(tpcb) > TCP_FIRST_SEND_SIZE)
{
/*transmit received data through TCP*/
/*第一小包数据,在数据前面插入帧头*/
if(fdma_buf.pkg_cnt==0)
{
bufaddr = (u8*)(RxBufferPtr[fdma_buf.record[fdma_buf.next]]);//获取地址
header_p = (packet_header *)bufaddr;
header_p->ID0 = HEADER_ID0;
header_p->ID1 = HEADER_ID1;
header_p->hsize = 1280;
header_p->vsize = 720;
header_p->tsize = IMG_SIZE;
header_p->framcnt = fdma_buf.fram_cnt;
header_p->psize = IMG_SIZE;
header_p->offset = 0;
/*确保cache一致性问题*/
Xil_DCacheInvalidateRange((u32)bufaddr + HEADER_SIZE, TCP_PACKEG_SIZE);
/*数据写入到TCP的发送缓存*/
err = tcp_write(tpcb, bufaddr, TCP_FIRST_SEND_SIZE, apiflags);
/*地址增加帧头偏移*/
bufaddr = bufaddr + HEADER_SIZE;
/*计算累计发送的大小*/
pkg_offset = pkg_offset + TCP_PACKEG_SIZE;
}
else if(fdma_buf.pkg_cnt < TCP_SEND_TIMES) //发送剩余包数据
{
/*计算下一包数据数据首地址*/
bufaddr = bufaddr + TCP_PACKEG_SIZE;
Xil_DCacheInvalidateRange((u32)bufaddr, TCP_PACKEG_SIZE);
err = tcp_write(tpcb, bufaddr, TCP_PACKEG_SIZE, apiflags);
pkg_offset = pkg_offset + TCP_PACKEG_SIZE;
}
else if(TCP_SEND_LAST_SIZE>0) //发送剩余包数据
{
bufaddr = bufaddr + TCP_PACKEG_SIZE;
Xil_DCacheInvalidateRange((u32)bufaddr, TCP_SEND_LAST_SIZE);
err = tcp_write(tpcb, bufaddr, TCP_SEND_LAST_SIZE, apiflags);
pkg_offset = pkg_offset + TCP_SEND_LAST_SIZE;
}
if (err != ERR_OK) {
xil_printf("txperf: Error on tcp_write: %d\r\n", err);
return;
}
err = tcp_output(tpcb);
if (err != ERR_OK) {
xil_printf("txperf: Error on tcp_output: %d\r\n",err);
return;
}
fdma_buf.pkg_cnt++;/*小包计数器*/
/*判断数据是否发送完毕*/
if(pkg_offset == IMG_SIZE)
{
pkg_offset=0;
fdma_buf.fram_cnt++;
fdma_buf.pkg_done_cnt--;
fdma_buf.pkg_cnt = 0;
if(fdma_buf.next<2)
fdma_buf.next++;
else
fdma_buf.next=0;
}
}
}
else if(fdma_buf.pkg_done_cnt > 2) //如果缓存不能处理,通过设置irst_trans_start = 0再次同步
{
xil_printf("error pkg_done_cnt = %d \r\n", fdma_buf.pkg_done_cnt);
first_trans_start = 0;
}
/*
if (client.end_time || client.i_report.report_interval_time) {
u64_t now = get_time_ms();
if (client.i_report.report_interval_time) {
if (client.i_report.start_time) {
u64_t diff_ms = now - client.i_report.start_time;
u64_t rtime_ms = client.i_report.report_interval_time;
if (diff_ms >= rtime_ms) {
tcp_conn_report(diff_ms, INTER_REPORT);
client.i_report.start_time = 0;
client.i_report.total_bytes = 0;
}
} else {
client.i_report.start_time = now;
}
}
}*/
return ERR_OK;
}
|
10:本地IP地址设置在主程序tcp_lwip_test.c中定义 #define DEFAULT_IP_ADDRESS "192.168.137.10"
#define DEFAULT_IP_MASK"255.255.255.0"
#define DEFAULT_GW_ADDRESS "192.168.1.1"
|
11:远程主机IP设置在头文件tcp_client.h中 #define TCP_SERVER_IP_ADDRESS "192.168.137.209"
#define TCP_CONN_PORT 5001
|
7方案演示
7.1硬件准备
7.2实验结果
|