zynq實現tcp網絡的數據包的上傳(zynq7z035)(超詳細版本)

 

 

 

1、新建工程

(1) 打開vivado軟件,選擇創建項目, 我的版本是2017版本

(2)選擇要建立的文件夾的位置,和工程名字

(3)新建一個RTL project 然後選擇下一步

(4) 選擇默認即可,關於硬件的描述語言選擇自己使用的。

(5)不添加任何文件,直接選擇下一步

 

(6) 選擇與自己硬件版本對應的參數

(7)最後一步,點擊完成當前的文件工程

2、新建相關arm的工程

(1)新建一個block design

(2)選擇工程名

(3) 添加zynq 的arm平臺

(4) 配置一下相關arm一些參數,取消相關M AXI 的接口

 

(5)確定系統時鐘頻率,保持默認即可

(6)修改ddr configuration ,在“DDR Configuration”選項卡中可以配置 PS 端 ddr 的參數,“Memory Part”選擇
“MT41J256M16 RE-125”,“Effective DRAM Bus Width”,選擇“32 Bit”,到此配置完成,

(7) 勾選系統中斷

 

(8) 使能HP0端口-----這一層去掉。。。。。

(9) 相關PS端的以太網的配置,使能“Enet0”和“MDIO”

同時爲了方便調試打開uart

 

 

(10) 保持 Enet0 的電平標準爲 LVCMOS1.8,點擊ok完成所有的配置

(11) run block automation 

 

(12) hdl wrapper

 

 

 

 

 

3、關於LWIP庫的文件修改相關

由於自帶的 LWIP 庫只能識別部分 phy 芯片,如果開發板所用的 phy 芯片不在默認支持範圍
內,要修改庫文件。也可以直接使用修改過的庫替換原有的庫

(1)找到文件所在位置

 

 

(2)找到要修改的庫的文件

在這兩個文件中都加入如下內容:

 

修改get_IEEE_phy_speed()


static u32_t get_IEEE_phy_speed(XEmacPs *xemacpsp, u32_t phy_addr)
{
	u16_t phy_identity;
	u32_t RetStatus;

	XEmacPs_PhyRead(xemacpsp, phy_addr, PHY_IDENTIFIER_1_REG,
					&phy_identity);
	if(phy_identity == MICREL_PHY_IDENTIFIER)
	{

		xil_printf("this is in xemacpsif_phy, this is micrel phy");
		xil_printf("this is addr %s");
		RetStatus=1000;
		//RetStatus = get_Marvell_phy_speed(xemacpsp, phy_addr);
		// RetStatus = get_Micrel_phy_speed(xemacpsp, phy_addr);
		//RetStatus = get_phy_speed_ksz9031(xemacpsp,phy_addr);
	} else 	if (phy_identity == PHY_TI_IDENTIFIER) {
		RetStatus = get_TI_phy_speed(xemacpsp, phy_addr);
	} else if (phy_identity == PHY_REALTEK_IDENTIFIER) {
		RetStatus = get_Realtek_phy_speed(xemacpsp, phy_addr);
	} else {
		RetStatus = get_Marvell_phy_speed(xemacpsp, phy_addr);
	}	

	return RetStatus;
}

 

ps:上面的RetStatus =1000 表示當前的連接的網卡是千兆。當前這樣寫並不完全正確,應當進入函數,但是當前先採用當前的這樣寫的方案

4、啓動sdk

(1)export hardware

 

 

 

(2)啓動sdk後

 

(3) 選擇一個lwip的應用程序

5  修改tcp的發送程序

5.1 lwip的基本流程

(1)選擇新建的system.mss , 然後點擊修改相關的內容,關掉dhcp

修改dhcp :

修改不在zynq平臺

 

(2) 主函數流程:

 

 

 

 

 

 

  1. 初始化所有lwip,用lwip_init()
  2. 添加以太網的MAC, 使用xemac_add
  3. Lwip 是基於中斷的,因此使能處理器的中斷。
  4. 設定一個定時器爲一個常數值,一般都設定再250ms。當中斷的時候去觸發tcp_fastmr 或者tcp_slowtmr
  5. 在整個模塊初始化後,主程序進入無線循環。

這個是一個套路,基本都按照這個進行

 

 

流程 如下:

 

(3) 主函數內容 流程

給出的例程是的主函數的實際代碼:


int main()
{
	struct ip_addr ipaddr, netmask, gw;

	/* the mac address of the board. this should be unique per board */
	unsigned char mac_ethernet_address[] =
	{ 0x00, 0x0a, 0x35, 0x00, 0x01, 0x02 };

	echo_netif = &server_netif;
#if defined (__arm__) && !defined (ARMR5)
#if XPAR_GIGE_PCS_PMA_SGMII_CORE_PRESENT == 1 || XPAR_GIGE_PCS_PMA_1000BASEX_CORE_PRESENT == 1
	ProgramSi5324();
	ProgramSfpPhy();
#endif
#endif

/* Define this board specific macro in order perform PHY reset on ZCU102 */
#ifdef XPS_BOARD_ZCU102
	IicPhyReset();
#endif

	init_platform();

#if LWIP_DHCP==1
    ipaddr.addr = 0;
	gw.addr = 0;
	netmask.addr = 0;
#else
	/* initliaze IP addresses to be used */
	IP4_ADDR(&ipaddr,  192, 168,   127, 202);
	IP4_ADDR(&netmask, 255, 255, 255,  0);
	IP4_ADDR(&gw,      192, 168,   127,  254);
#endif	
	print_app_header();

	lwip_init();

  	/* Add network interface to the netif_list, and set it as default */
	if (!xemac_add(echo_netif, &ipaddr, &netmask,
						&gw, mac_ethernet_address,
						PLATFORM_EMAC_BASEADDR)) {
		xil_printf("Error adding N/W interface\n\r");
		return -1;
	}
	netif_set_default(echo_netif);

	/* now enable interrupts */
	platform_enable_interrupts();

	/* specify that the network if is up */
	netif_set_up(echo_netif);

#if (LWIP_DHCP==1)
	/* Create a new DHCP client for this interface.
	 * Note: you must call dhcp_fine_tmr() and dhcp_coarse_tmr() at
	 * the predefined regular intervals after starting the client.
	 */
	dhcp_start(echo_netif);
	dhcp_timoutcntr = 24;

	while(((echo_netif->ip_addr.addr) == 0) && (dhcp_timoutcntr > 0))
		xemacif_input(echo_netif);

	if (dhcp_timoutcntr <= 0) {
		if ((echo_netif->ip_addr.addr) == 0) {
			xil_printf("DHCP Timeout\r\n");
			xil_printf("Configuring default IP of 192.168.1.10\r\n");
			IP4_ADDR(&(echo_netif->ip_addr),  192, 168,   1, 10);
			IP4_ADDR(&(echo_netif->netmask), 255, 255, 255,  0);
			IP4_ADDR(&(echo_netif->gw),      192, 168,   1,  1);
		}
	}

	ipaddr.addr = echo_netif->ip_addr.addr;
	gw.addr = echo_netif->gw.addr;
	netmask.addr = echo_netif->netmask.addr;
#endif

	print_ip_settings(&ipaddr, &netmask, &gw);

	/* start the application (web server, rxtest, txtest, etc..) */
	start_application();

	/* receive and process packets */
	while (1) {
		if (TcpFastTmrFlag) {
			tcp_fasttmr();
			TcpFastTmrFlag = 0;
		}
		if (TcpSlowTmrFlag) {
			tcp_slowtmr();
			TcpSlowTmrFlag = 0;
		}
		xemacif_input(echo_netif);
		transfer_data();
	}
  
	/* never reached */
	cleanup_platform();

	return 0;
}

將上面的算法進行一定的簡化,簡化後的代碼如下:


int main()
{
	struct ip_addr ipaddr, netmask, gw;

	/* the mac address of the board. this should be unique per board */
	unsigned char mac_ethernet_address[] =
	{ 0x00, 0x0a, 0x35, 0x00, 0x01, 0x02 };

	echo_netif = &server_netif;
#if defined (__arm__) && !defined (ARMR5)
#if XPAR_GIGE_PCS_PMA_SGMII_CORE_PRESENT == 1 || XPAR_GIGE_PCS_PMA_1000BASEX_CORE_PRESENT == 1
	ProgramSi5324();
	ProgramSfpPhy();
#endif
#endif

/* Define this board specific macro in order perform PHY reset on ZCU102 */
#ifdef XPS_BOARD_ZCU102
	IicPhyReset();
#endif

	init_platform();

	/* initliaze IP addresses to be used */
	IP4_ADDR(&ipaddr,  192, 168,   127, 202);
	IP4_ADDR(&netmask, 255, 255, 255,  0);
	IP4_ADDR(&gw,      192, 168,   127,  254);
	print_app_header();
	lwip_init();
  	/* Add network interface to the netif_list, and set it as default */
	if (!xemac_add(echo_netif, &ipaddr, &netmask,
						&gw, mac_ethernet_address,
						PLATFORM_EMAC_BASEADDR)) {
		xil_printf("Error adding N/W interface\n\r");
		return -1;
	}
	netif_set_default(echo_netif);

	/* now enable interrupts */
	platform_enable_interrupts();

	/* specify that the network if is up */
	netif_set_up(echo_netif);

	print_ip_settings(&ipaddr, &netmask, &gw);

	/* start the application (web server, rxtest, txtest, etc..) */
	start_application();

	/* receive and process packets */
	while (1) {
		if (TcpFastTmrFlag) {
			tcp_fasttmr();
			TcpFastTmrFlag = 0;
		}
		if (TcpSlowTmrFlag) {
			tcp_slowtmr();
			TcpSlowTmrFlag = 0;
		}
		xemacif_input(echo_netif);
		transfer_data();
	}
  
	/* never reached */
	cleanup_platform();

	return 0;
}

 

(4)  注意ip設定

 

上面我設定的ip是192.168.127.202 ,我本地的電腦的設定的ip是192.168.127.200

 

6  下載程序到硬件平臺上面:

連接上位機的串口調試助手:

上位機打開tcp客戶端:

 

可以看到下位機的設備,

 

 

 

 

 

 

7、上位機速度測試:

7.1 代碼修改:

man.c 函數文件:

/******************************************************************************
*
* Copyright (C) 2009 - 2014 Xilinx, Inc.  All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* Use of the Software is limited solely to applications:
* (a) running on a Xilinx device, or
* (b) that interact with a Xilinx device through a bus or interconnect.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* XILINX  BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
* OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Except as contained in this notice, the name of the Xilinx shall not be used
* in advertising or otherwise to promote the sale, use or other dealings in
* this Software without prior written authorization from Xilinx.
*
******************************************************************************/

#include <stdio.h>

#include "xparameters.h"

#include "netif/xadapter.h"

#include "platform.h"
#include "platform_config.h"
#if defined (__arm__) || defined(__aarch64__)
#include "xil_printf.h"
#endif
#include "sleep.h"

#include "lwip/tcp.h"
#include "xil_cache.h"

#if LWIP_DHCP==1
#include "lwip/dhcp.h"
#endif

/* defined by each RAW mode application */
void print_app_header();
int start_application();
int transfer_data();
void tcp_fasttmr(void);
void tcp_slowtmr(void);

/* missing declaration in lwIP */
void lwip_init();

#if LWIP_DHCP==1
extern volatile int dhcp_timoutcntr;
err_t dhcp_start(struct netif *netif);
#endif

extern volatile int TcpFastTmrFlag;
extern volatile int TcpSlowTmrFlag;
extern volatile unsigned tcp_client_connected;
static struct netif server_netif;
struct netif *echo_netif;

void
print_ip(char *msg, struct ip_addr *ip) 
{
	print(msg);
	xil_printf("%d.%d.%d.%d\n\r", ip4_addr1(ip), ip4_addr2(ip), 
			ip4_addr3(ip), ip4_addr4(ip));
}

void
print_ip_settings(struct ip_addr *ip, struct ip_addr *mask, struct ip_addr *gw)
{

	print_ip("Board IP: ", ip);
	print_ip("Netmask : ", mask);
	print_ip("Gateway : ", gw);
}

#if defined (__arm__) && !defined (ARMR5)
#if XPAR_GIGE_PCS_PMA_SGMII_CORE_PRESENT == 1 || XPAR_GIGE_PCS_PMA_1000BASEX_CORE_PRESENT == 1
int ProgramSi5324(void);
int ProgramSfpPhy(void);
#endif
#endif

#ifdef XPS_BOARD_ZCU102
#ifdef XPAR_XIICPS_0_DEVICE_ID
int IicPhyReset(void);
#endif
#endif

int main()
{
	struct ip_addr ipaddr, netmask, gw;

	/* the mac address of the board. this should be unique per board */
	unsigned char mac_ethernet_address[] =
	{ 0x00, 0x0a, 0x35, 0x00, 0x01, 0x02 };

	echo_netif = &server_netif;
#if defined (__arm__) && !defined (ARMR5)
#if XPAR_GIGE_PCS_PMA_SGMII_CORE_PRESENT == 1 || XPAR_GIGE_PCS_PMA_1000BASEX_CORE_PRESENT == 1
	ProgramSi5324();
	ProgramSfpPhy();
#endif
#endif

/* Define this board specific macro in order perform PHY reset on ZCU102 */
#ifdef XPS_BOARD_ZCU102
	IicPhyReset();
#endif

	//TO enable platform interupt
	init_platform();

	/* initliaze IP addresses to be used */
	IP4_ADDR(&ipaddr,  192, 168,   127, 202);
	IP4_ADDR(&netmask, 255, 255, 255,  0);
	IP4_ADDR(&gw,      192, 168,   127,  254);
	print_app_header();
	lwip_init();
  	/* Add network interface to the netif_list, and set it as default */
	if (!xemac_add(echo_netif, &ipaddr, &netmask,
						&gw, mac_ethernet_address,
						PLATFORM_EMAC_BASEADDR)) {
		xil_printf("Error adding N/W interface\n\r");
		return -1;
	}
	netif_set_default(echo_netif);

	/* now enable interrupts */
	platform_enable_interrupts();

	/* specify that the network if is up */
	netif_set_up(echo_netif);

	print_ip_settings(&ipaddr, &netmask, &gw);

	/* start the application (web server, rxtest, txtest, etc..) */
	start_application();

	/* receive and process packets */
	while (1) {
		if (TcpFastTmrFlag) {
			tcp_fasttmr();
			TcpFastTmrFlag = 0;
		}
		if (TcpSlowTmrFlag) {
			tcp_slowtmr();
			TcpSlowTmrFlag = 0;
		}
		xemacif_input(echo_netif);
		if (tcp_client_connected) {  //連接成功則發送數據
			usleep(100);
			transfer_data();

		}

	}
  
	/* never reached */
	cleanup_platform();

	return 0;
}

echo.c 文件:

/******************************************************************************
*
* Copyright (C) 2009 - 2014 Xilinx, Inc.  All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* Use of the Software is limited solely to applications:
* (a) running on a Xilinx device, or
* (b) that interact with a Xilinx device through a bus or interconnect.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* XILINX  BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
* OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Except as contained in this notice, the name of the Xilinx shall not be used
* in advertising or otherwise to promote the sale, use or other dealings in
* this Software without prior written authorization from Xilinx.
*
******************************************************************************/

#include <stdio.h>
#include <string.h>

#include "lwip/err.h"
#include "lwip/tcp.h"
#if defined (__arm__) || defined (__aarch64__)
#include "xil_printf.h"
#endif
static struct tcp_pcb *connected_pcb = NULL;
char sendBuffer[10]="HELLOWORLD";
volatile unsigned tcp_client_connected = 0;

int transfer_data() {
	err_t err;
	struct tcp_pcb *tpcb = connected_pcb;
	//xil_printf("we ccan in transfer data\r\n", err);
	if (!connected_pcb)
			return;

	err = tcp_write(tpcb, sendBuffer, 10, 3);
	if (err != ERR_OK) {
		xil_printf("txperf: Error on tcp_write: %d\r\n", err);
		connected_pcb = NULL;
		return;
	}
	err = tcp_output(tpcb);
	if (err != ERR_OK) {
		xil_printf("txperf: Error on tcp_output: %d\r\n",err);
		return;
	}


	return 0;
}

void print_app_header()
{
	xil_printf("\n\r\n\r-----lwIP TCP echo server ------\n\r");
	xil_printf("TCP packets sent to port 6001 will be echoed back\n\r");
}

err_t recv_callback(void *arg, struct tcp_pcb *tpcb,
                               struct pbuf *p, err_t err)
{
	/* do not read the packet if we are not in ESTABLISHED state */
	if (!p) {
		tcp_close(tpcb);
		tcp_recv(tpcb, NULL);
		return ERR_OK;
	}

	/* indicate that the packet has been received */
	tcp_recved(tpcb, p->len);

	/* echo back the payload */
	/* in this case, we assume that the payload is < TCP_SND_BUF */
	if (tcp_sndbuf(tpcb) > p->len) {
		err = tcp_write(tpcb, p->payload, p->len, 1);
	} else
		xil_printf("no space in tcp_sndbuf\n\r");

	/* free the received pbuf */
	pbuf_free(p);

	return ERR_OK;
}

err_t accept_callback(void *arg, struct tcp_pcb *newpcb, err_t err)
{
	static int connection = 1;

	/* set the receive callback for this connection */
	tcp_recv(newpcb, recv_callback);

	/* just use an integer number indicating the connection id as the
	   callback argument */
	tcp_arg(newpcb, (void*)(UINTPTR)connection);

	/* increment for subsequent accepted connections */
	connection++;

	//將新這個接收的這個accept 賦值給全局變量,以便我們進行其他的處理,例如主動發送數據
	connected_pcb = newpcb;
	//禁用nagle 算法
	tcp_nagle_disable(connected_pcb);
	// zhiwei
	tcp_client_connected=1;

	return ERR_OK;
}


int start_application()
{
	struct tcp_pcb *pcb;
	err_t err;
	unsigned port = 5011;

	/* create new TCP PCB structure */
	pcb = tcp_new();
	if (!pcb) {
		xil_printf("Error creating PCB. Out of Memory\n\r");
		return -1;
	}

	/* bind to specified @port */
	err = tcp_bind(pcb, IP_ADDR_ANY, port);
	if (err != ERR_OK) {
		xil_printf("Unable to bind to port %d: err = %d\n\r", port, err);
		return -2;
	}

	/* we do not need any arguments to callback functions */
	tcp_arg(pcb, NULL);

	/* listen for connections */
	pcb = tcp_listen(pcb);
	if (!pcb) {
		xil_printf("Out of memory while tcp_listen\n\r");
		return -3;
	}

	/* specify callback to use for incoming connections */
	tcp_accept(pcb, accept_callback);

	xil_printf("TCP echo server started @ port %d\n\r", port);

	return 0;
}

具體額外的下位機代碼工程,參見github地址:https://github.com/Scottars/TCP_UDPtranformonZYNQ

7.2 下位機的延時100us發送

(1)下位機設定爲100us 的延時

(2)  上位機的接收代碼


    client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # 建立連接,這個建立的是tcp的鏈接
    # client_socket.setsockopt(socket.IPPROTO_TCP,socket.TCP_NODELAY,True)
    IP_Server='192.168.127.202'
    Port=5011
    client_socket.connect((IP_Server,Port))





    # s.connect(('192.168.127.5', 5001))
    print('we have connected to the tcp data send server!---port is :', port)

    packagenum = 0

    zhanbao = 0
    buzhanbao = 0
    start_time_perf = time.perf_counter()
    start_time_process = time.process_time()
    count = 0
    # 實際上應當啓用的市多線程來做這些事情的
    # 每一個線程要做的事情就是接收對應的內容
    # 我想epics裏面做的也是基本想同樣的事情  ---最後寫一個自動化的腳本多線程
    start_flag = True
    while True:

        # try:  #許久沒有接收到下位機來的消息,首先會有一個keep  alive 的數據包的出現,如果太久沒有了就直接關閉當前socket
        b = client_socket.recv(10)
        print('I',count,'b:',b)

        if count==1000000:
            break
        # size = len(b)
        count = count + 1
        # timestample = str(datetime.datetime.now()).encode()
        # b = b + timestample
        #

(3) 測試結果

上位機能夠正常的接收到所有的數據,當上位機的數據接收到1M個數據的售後將會跳出來

接收耗時約爲100s,可見發送速度也就約爲10k/s。

 

7.3 下位機延時10us 發送

(1)下位機延時10us

(2) 上位機接收程序

(3) 測試結果:

  第一次測試:        第二次測試:    

 

7.4 下位機延時設設定50us

(1) 下位機設定延時50us

(2) 上位機的測試的程序

(3)測試結果:

7.5 提高延時設定,爲80us

(1)設定延時爲80us的獅虎的測試的結果:

7.6 降低延時爲並不設定延時

測試結果:

7.6 原因查找:

仔細查看了一個下代碼:

參考文章:https://blog.csdn.net/FPGADesigner/article/details/88776615

7.7 解決方案

採用上述所說的輪詢機制:https://blog.csdn.net/FPGADesigner/article/details/88778125

根據上面的這篇的blog 中的內容,可以選擇poll 輪詢的機制,但是可以看到tcp_toll 用於指定輪詢的間隔。

根據函數的描述如下:

由於 我們希望我們上傳的速度   應當是儘可能的快的,而根據上述對輪詢的間隔,有每秒發送一次的速度,這樣的速度不能夠滿足我們的要求,目前還未有可解決的方案。-

-------------------2020-05-29

-----------------------未完待續-------------------------------------

 

--update- on 2020-06-01

7.8 根本原因溯源

   我們看到我們串口打印的數據的實際上出現在tcp_write 這句話的函數上面。如下:

進一步查看tcpwrite 中報錯具體是哪一句話:

進一步查看tcp_write_check 函數的報錯

通過上面圖片中的代碼,我們可以得出的兩點原因如下:

原因1:由於發送的數據太多。

原因2:在未發送或者未確認的隊列中的pubfs 的總數,超過了所配置的大小;

7.9 查看在lwip中的配置界面,嘗試修改這一塊的配置

我們看一下上面代碼中的 TCP_SND_QUEUELEN的定義:

pcb->snd_queuelen >= TCP_SND_QUEUELEN

定義如下:

我們再進而查看:lwip的配置界面:

通是修改memp_n_pbuf的大小,將從原來默認的16,變成1600,再次進行tcp的數據的測試。

 

更多的測試結果請持續關注我的其他的blog的內容。

同時也歡迎大家更多的訂閱,一起在csdn上遨遊。

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章