嗨!别来无恙,这里是码龄区区一年的小菜鸡。
目录
- 实验概述
- 一、LAN8720A简介
- 二、TCP/IP简介
- 三、硬件设计
- 1.串口
- 2.LCD模块
- 3.ETH
- 4.PCF8574
- 四、软件设计
- 1.tcp_server_thread()
- 2.tcp_server_init()
- 3.main.c
- 五、下载验证
- 六、总结
实验概述
本实验所用硬件为正点原子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系统,脑子里还是在捣糨糊。
慢慢来吧。