串口(CH340)与网口数据互传实验

嗨!别来无恙,这里是码龄区区一年的小菜鸡。

实验概述

  本实验所用硬件为正点原子STM32F4/F7系列APOLLO开发板,主芯片为STM32F767ICT6,外部PHY芯片为开发板自带的LAN8720A,配合LWIP TCP/IP协议栈实现TCP服务器功能。

一、LAN8720A简介

  LAN8720A是低功耗的10/100M以太网PHY层芯片,I/O引脚电压符合IEEE802.3-2005标准,支持通过RMII接口与以太网MAC层通信,内置10-BASE-T/100BASE-TX全双工传输模块,支持10Mps和100Mpbs。

  LAN8720A可以通过自协商的方式与目的主机进行最佳的连接方式(速度和双工模式),支持HP Auto-MDIX自动翻转功能,无需更换网线即可将连接更改为直连或交叉连接。LAN8720A的主要特点如下:

  高性能的10/100M以太网传输模块
  支持RMII接口以减少引脚数
  支持全双工和半双工模式
  两个状态LED输出
  可以使用25M晶振以降低成本
  支持自协商模式
  支持HP Auto-MDIX自动翻转功能
  支持SMI串行接口管理
  支持MAC接口

LAN8720A功能框图如下图所示:

在这里插入图片描述

二、TCP/IP简介

  TCP/IP中文名为传输控制协议/因特网互联协议,又名网络通讯协议,是Internet最基本的协议、Interent国际互联网络的基础,由网络层的IP协议和传输层的TCP协议组成。TCP/IP定义了电子设备如何连入因特网,以及数据如何在它们之间传输的标准。协议采用了4层的层级结构,每一层都呼叫它的下一层所提供的协议来完成自己的需求。通俗而言:TCP负责发现传输的问题,一有问题就发出信号,要求重新传输,直到所有数据安全正确的传输到目的地。而IP是给因特网的每一台联网设备规定一个地址。

  TCP/IP协议不是TCP和IP这两个协议的合称,而是指因特网整个TCP/IP协议簇。从协议分层模型方面来讲,TCP/IP由四个层组成:网络接口层、网络层、传输层、应用层。OSI是传统的开放式系统互联参考模型,该模型将TCP/IP分为七层:物理层、数据链路层(网络接口层)、网络层、传输层、会话层、表示层和应用层。TCP/IP模型与OSI模型对比如下图所示:
在这里插入图片描述
  具体来说,本实验中的PHY层芯片LAN8720A相当于物理层,STM32F7自带的MAC层相当于数据链路层,而LWIP提供的就是网络层、传输层的功能,应用层是需要用户自己根据想要实现的功能去设置的。

三、硬件设计

  本实验需要用到的主要硬件资源有:串口、LCD模块、ETH(STM32F7自带以太网功能)、LAN8720A、PCF8574(IIC并行扩展模块)

  这里只给出原理图,不再过多赘述,想深究的小伙伴可以去正点原子官方网站下载STM32F7的 资料,有很详细的讲解。

1.串口

在这里插入图片描述

2.LCD模块

在这里插入图片描述

3.ETH

在这里插入图片描述

4.PCF8574

在这里插入图片描述

四、软件设计

  本实验最主要的两个文件为tcp_server_demo.c和tcp_server_demo.h,也即本实验的源码。在tcp_server_demo.c中定义了两个函数tcp_server_thread()和tcp_server_init(),在有操作系统的支持下,TCP服务器可以作为一个线程来处理。

1.tcp_server_thread()

  tcp_server_thread()函数为TCP服务器任务函数,定义一个全局变量tcp_server_flag来标记是否有数据发送,当有数据发送时tcp_server_flag的bit8就为1,通过与LWIP_SEND_DATA进行与运算就知道是否有数据要发送,有数据要发送就调用netconn_write()函数将tcp_server_sendbuf中的数据发送出去。

代码如下:

static void tcp_server_thread(void *arg)
{
	OS_CPU_SR cpu_sr;
	u32 data_len = 0;
	struct pbuf *q;
	err_t err,recv_err;
	u8 remot_addr[4];
	struct netconn *conn, *newconn;
	static ip_addr_t ipaddr;
	static u16_t port;
	
	LWIP_UNUSED_ARG(arg);

	conn=netconn_new(NETCONN_TCP);  //创建一个TCP链接
	netconn_bind(conn,IP_ADDR_ANY,TCP_SERVER_PORT);  //绑定端口 8088号端口
	netconn_listen(conn);  		//进入监听模式
	conn->recv_timeout = 10;  	//禁止阻塞线程 等待10ms
	while (1) 
	{
		err = netconn_accept(conn,&newconn);  //接收连接请求
		if(err==ERR_OK)newconn->recv_timeout = 10;

		if (err == ERR_OK)    //处理新连接的数据
		{ 
			struct netbuf *recvbuf;

			netconn_getaddr(newconn,&ipaddr,&port,0); //获取远端IP地址和端口号
			
			remot_addr[3] = (uint8_t)(ipaddr.addr >> 24); 
			remot_addr[2] = (uint8_t)(ipaddr.addr>> 16);
			remot_addr[1] = (uint8_t)(ipaddr.addr >> 8);
			remot_addr[0] = (uint8_t)(ipaddr.addr);
			printf("主机%d.%d.%d.%d连接上服务器,主机端口号为:%d\r\n",remot_addr[0], remot_addr[1],remot_addr[2],remot_addr[3],port);
			
			while(1)
			{
				if((tcp_server_flag & LWIP_SEND_DATA) == LWIP_SEND_DATA) //有数据要发送
				{
					err = netconn_write(newconn ,tcp_server_sendbuf,strlen((char*)tcp_server_sendbuf),NETCONN_COPY); //发送tcp_server_sendbuf中的数据
					if(err != ERR_OK)
					{
						printf("发送失败\r\n");
					}
					tcp_server_flag &= ~LWIP_SEND_DATA;
				}
				
				if((recv_err = netconn_recv(newconn,&recvbuf)) == ERR_OK)  	//接收到数据
				{		
					OS_ENTER_CRITICAL(); //关中断
					memset(tcp_server_recvbuf,0,TCP_SERVER_RX_BUFSIZE);  //数据接收缓冲区清零
					for(q=recvbuf->p;q!=NULL;q=q->next)  //遍历完整个pbuf链表
					{
						//判断要拷贝到TCP_SERVER_RX_BUFSIZE中的数据是否大于TCP_SERVER_RX_BUFSIZE的剩余空间,如果大于
						//的话就只拷贝TCP_SERVER_RX_BUFSIZE中剩余长度的数据,否则的话就拷贝所有的数据
						if(q->len > (TCP_SERVER_RX_BUFSIZE-data_len)) memcpy(tcp_server_recvbuf+data_len,q->payload,(TCP_SERVER_RX_BUFSIZE-data_len));//拷贝数据
						else memcpy(tcp_server_recvbuf+data_len,q->payload,q->len);
						data_len += q->len;  	
						if(data_len > TCP_SERVER_RX_BUFSIZE) break; //超出TCP客户端接收数组,跳出	
					}
					OS_EXIT_CRITICAL();  //开中断
					data_len=0;  //复制完成后data_len要清零。	
					printf("%s\r\n",tcp_server_recvbuf);  //通过串口输出接收到的数据
					netbuf_delete(recvbuf);
				}else if(recv_err == ERR_CLSD)  //关闭连接
				{
					netconn_close(newconn);
					netconn_delete(newconn);
					printf("主机:%d.%d.%d.%d断开与服务器的连接\r\n",remot_addr[0], remot_addr[1],remot_addr[2],remot_addr[3]);
					break;
				}
				}
			}
		}
}

2.tcp_server_init()

  tcp_server_init()为创建TCP服务器线程,这个函数比较简单,不再展开。

代码如下:

//创建TCP服务器线程
//返回值:0 TCP服务器创建成功
//		其他 TCP服务器创建失败
INT8U tcp_server_init(void)
{
	INT8U res;
	OS_CPU_SR cpu_sr;
	
	OS_ENTER_CRITICAL();	//关中断
	res = OSTaskCreate(tcp_server_thread,(void*)0,(OS_STK*)&TCPSERVER_TASK_STK[TCPSERVER_STK_SIZE-1],TCPSERVER_PRIO); //创建TCP服务器线程
    OS_EXIT_CRITICAL();		//开中断
	
	return res;
}

3.main.c

//串口转网口任务
#define USART_TO_TCP_TASK_PRIO   8
#define USART_TO_TCP_STK_SIZE    128
OS_STK USART_TO_TCP_TASK_STK[USART_TO_TCP_STK_SIZE];
void usart_to_tcp_task(void *pdata);

//在LCD上显示地址信息任务
//任务优先级
#define DISPLAY_TASK_PRIO	9
//任务堆栈大小
#define DISPLAY_STK_SIZE	128
//任务堆栈
OS_STK	DISPLAY_TASK_STK[DISPLAY_STK_SIZE];
//任务函数
void display_task(void *pdata);


//START任务
//任务优先级
#define START_TASK_PRIO		10
//任务堆栈大小
#define START_STK_SIZE		128
//任务堆栈
OS_STK START_TASK_STK[START_STK_SIZE];
//任务函数
void start_task(void *pdata); 

//在LCD上显示地址信息
//mode:1 显示DHCP获取到的地址
//	  其他 显示静态地址
void show_address(u8 mode)
{
	u8 buf[30];
	sprintf((char*)buf,"Static IP:%d.%d.%d.%d",lwipdev.ip[0],lwipdev.ip[1],lwipdev.ip[2],lwipdev.ip[3]);						//打印静态IP地址
	LCD_ShowString(30,150,210,16,16,buf); 
	sprintf((char*)buf,"Static GW:%d.%d.%d.%d",lwipdev.gateway[0],lwipdev.gateway[1],lwipdev.gateway[2],lwipdev.gateway[3]);	//打印网关地址
	LCD_ShowString(30,170,210,16,16,buf); 
	sprintf((char*)buf,"NET MASK:%d.%d.%d.%d",lwipdev.netmask[0],lwipdev.netmask[1],lwipdev.netmask[2],lwipdev.netmask[3]);	//打印子网掩码地址
	LCD_ShowString(30,190,210,16,16,buf); 
	LCD_ShowString(30,210,210,16,16,"Port:8088"); 
}

int main(void)
{
    Write_Through();                //开启强制透写!
    Cache_Enable();                 //打开L1-Cache
    
    HAL_Init();				        //初始化HAL库
    Stm32_Clock_Init(360,25,2,9);   //设置时钟,216Mhz 
    delay_init(180);                //延时初始化
	uart_init(115200);		        //串口初始化
    SDRAM_Init();                   //初始化SDRAM
    LCD_Init();                     //初始化LCD
    PCF8574_Init();                 //初始化PCF8574
    my_mem_init(SRAMIN);		    //初始化内部内存池
	my_mem_init(SRAMEX);		    //初始化外部内存池
	my_mem_init(SRAMDTCM);		    //初始化DTCM内存池
	POINT_COLOR = RED; 		        //红色字体
	LCD_ShowString(30,50,200,20,16,"TCP Server Test");
    
	OSInit(); 					    //UCOS初始化
	while(lwip_comm_init()) 	    //lwip初始化
	{
		LCD_ShowString(30,110,200,20,16,"Lwip Init failed! "); 	//lwip初始化失败
		delay_ms(500);
		LCD_Fill(30,110,230,150,WHITE);
		delay_ms(500);
	}
    while(tcp_server_init()) 									//初始化tcp_server(创建tcp_server程)
	{
		LCD_ShowString(30,110,200,20,16,"TCP Server failed! "); 
		delay_ms(500);
		LCD_Fill(30,110,230,170,WHITE);
		delay_ms(500);
	}
    LCD_ShowString(30,110,200,20,16,"TCP Server Success!"); 	//tcp服务器创建成功
	OSTaskCreate(start_task,(void*)0,(OS_STK*)&START_TASK_STK[START_STK_SIZE-1],START_TASK_PRIO);
	OSStart(); //开启UCOS						  	       
}

//start任务
void start_task(void *pdata)
{
	OS_CPU_SR cpu_sr;
	pdata = pdata ;
	
	OSStatInit();  			//初始化统计任务
	OS_ENTER_CRITICAL();  	//关中断
	
	OSTaskCreate(usart_to_tcp_task,(void*)0,(OS_STK*)&USART_TO_TCP_TASK_STK[USART_TO_TCP_STK_SIZE-1],USART_TO_TCP_TASK_PRIO);//串口转网口任务
	OSTaskCreate(display_task,(void*)0,(OS_STK*)&DISPLAY_TASK_STK[DISPLAY_STK_SIZE-1],DISPLAY_TASK_PRIO); //显示任务
	OSTaskSuspend(OS_PRIO_SELF); //挂起start_task任务
	OS_EXIT_CRITICAL();  		//开中断
}

//显示地址等信息
void display_task(void *pdata)
{
	while(1)
	{ 
	    show_address(0); 						//显示静态地址
		OSTaskSuspend(OS_PRIO_SELF); 		 	//显示完地址信息后挂起自身任务

		OSTimeDlyHMSM(0,0,0,500);
	}
}

//串口转网口任务
void usart_to_tcp_task(void *pdata)
{
	u8 len;
	while(1)
	{
		if(USART_RX_STA&0x8000)
		{
			len=USART_RX_STA&0x3fff;//得到此次接收到的数据长度		
			tcp_server_flag |= LWIP_SEND_DATA;//发送数据	
			USART_RX_BUF[USART_RX_STA&0X3fff]=0;
			USART_RX_STA=0;	
			OSTimeDlyHMSM(0,0,0,10);
		}	
	}	
}

  main函数的主要功能是给LWIP提供时钟,然后通过调用lwip_comm_init()函数,初始化LWIP,该函数处理包括:初始化STM32F767的以太网外设、初始化LAN8720A、分配内存、添加并打开网卡等操作。

这里需要特别注意:因为配置STM32F767的网卡使用自动协商功能(双工模式和连接速度),如果协商过程中遇到问题,则会进行多次重试,需要等待很久,而且如果协商失败,那么直接返回错误,导致LWIP初始化失败。因此一定要插上网线,LWIP才能初始化成功

五、下载验证

  在下载测试之前,先用网线将开发板和PC端网口相连,如此获取到的是静态IP。也可以用两根网线分别连接开发板网口和路由器、PC端网口和路由器,如此获取到的是动态DHCP。这里不想拖两根线出来,也不想再配置获取DHCP的操作,就偷个懒直接将开发板和PC通过网线互通了。总之,保证开发板和PC处于同一局域网下即可。

  如果是用网线连接开发板和PC的,还需要设置一些电脑的本地连接属性,设置如下图:
在这里插入图片描述
  这里设置的是Internet协议版本4(TCP/IPv4),对于IP地址192.168.1.100,最后的100可以是除1和30之外的任意数字。设置完成后开发板和PC就可以互相通信了。

最后,大坑来了!!!
  如果电脑上装有虚拟机的小伙伴,在下载测试之前,一定要把跟虚拟机(VMvare)相连的两个网络禁用,要不然接下来的测试中TCP网络调试助手是无法连接的。像下图这样禁用即可:
在这里插入图片描述
  小白我当时也是被这个问题烦了好久,一度想重装系统,好在磕磕绊绊下解决了。原因在于虚拟机的网络与电脑的以太网或WIFI网络不是同一个网络,所以网络调试助手无法辨别到底是那个本地IP需要与服务器连接,所以就会左下角显示“1035 未知错误”。见下图:
在这里插入图片描述
好了,一切就绪,开始测试。

  将代码烧写到开发板中,打开串口调试工具和网络调试工具。在串口调试工具窗口会显示网络配置信息,按此信息配置网络调试工具属性,配置正确后就可以连接上了。同时,LCD液晶显示屏上也会显示相关网络配置信息,如下图:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

本实验中开发板作为服务器、PC作为客户端。下面测试数据能否互传,结果如下图:

在这里插入图片描述
发现串口调试助手和网络调试助手可以互发互收信息,基本实现本实验功能。

六、总结

  第一次耐心写完一篇博客,以往都是草草写几个字到最后都是不了了之。从开始做这个实验到基本能实现功能再到写下这篇博客,过程中收获还是蛮多的,对于TCP/IP协议有了全局上的一个了解,不至于像最初ping个局域网都不会。也有懵懵懂懂的地方,比如UCOSII系统,脑子里还是在捣糨糊。

  慢慢来吧。

  • 6
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
当你使用USB转串口CH340发送数据时,数据输过程如下: 1. 你将需要发送的数据通过串口发送命令发送给USB转串口芯片CH340。 2. CH340芯片将串口数据转换为并口数据,并通过USB接口发送到计算机。 3. 计算机接收到数据后,通过串口发送命令将数据发送到目标设备。 4. 目标设备接收到数据后,将ACK信号发送回CH340芯片。 5. CH340芯片接收到ACK信号后,将其转换为串口数据,并通过USB接口发送到计算机。 6. 计算机接收到ACK信号后,就知道数据已经成功发送到目标设备,发送过程结束。 当你使用USB转串口CH340接收数据时,数据输过程如下: 1. 目标设备发送数据到计算机。 2. CH340芯片将并口数据转换为串口数据,并通过USB接口发送到计算机。 3. 计算机接收到数据后,通过串口发送命令将数据发送给应用程序。 4. 应用程序接收到数据后,进行处理并给出响应。 5. 计算机将响应数据发送回USB转串口芯片CH340。 6. CH340芯片将串口数据转换为并口数据,并将其发送到目标设备。 7. 目标设备接收到数据后,将ACK信号发送回CH340芯片。 8. CH340芯片接收到ACK信号后,将其转换为串口数据,并通过USB接口发送到计算机。 9. 计算机接收到ACK信号后,就知道数据已经成功发送到目标设备,接收过程结束。 在这个过程中,USB转串口芯片CH340起到了重要的作用,它将串口数据转换为USB数据,并将USB数据转换为串口数据,实现了计算机和目标设备之间的数据输。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值