文档课程分类-AMD-ZYNQ
AMD-ZYNQ: |
ZYNQ-SOC » 1_SDK应用方案(仅旗舰型号) » 2-SDK高级应用方案 |
本帖最后由 FPGA课程 于 2024-10-10 18:19 编辑
软件版本: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 udp 方式把 DDR 中数据发送到上位机,并且显示。
实验目的:
1:掌握 uifdma_dbuf 配置成视频模式的情况下的参数设置
2:掌握 OV5640 摄像头数据如何通过 uifdma_dbuf 写入到 uifdma ip
3:ps 实现 uifdma_dbuf 中断到来,读取 DDR 缓存中的数据
4:设计基于 UDP 方式的摄像头传输方案
5:使用 lwip udp 方式把采集的摄像头数据从 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 次。
- #define TCP_PACKEG_SIZE
- 1024*16
- #define IMG_SIZE
- 1280*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 实验结果
|
|