当前位置: 首页 > news >正文

网站登录 效果代码南宁网站运营哪家好

网站登录 效果代码,南宁网站运营哪家好,网站建设哈尔滨,猫咪地域网名入口目录 NTP 协议实验NTP 简介NTP 实验硬件设计软件设计下载验证 lwIP 测试网速JPerf 网络测速工具JPerf 网络实验硬件设计软件设计下载验证 HTTP 服务器实验HTTP 协议简介HTTP 服务器实验硬件设计下载验证 网络摄像头#xff08;ATK-MC5640#xff09;实验ATK-MC5640 简介SCCB … 目录 NTP 协议实验NTP 简介NTP 实验硬件设计软件设计下载验证 lwIP 测试网速JPerf 网络测速工具JPerf 网络实验硬件设计软件设计下载验证 HTTP 服务器实验HTTP 协议简介HTTP 服务器实验硬件设计下载验证 网络摄像头ATK-MC5640实验ATK-MC5640 简介SCCB 简介OV5640 DVP 接口说明OV5640 窗口设置说明OV5640 行像素输出时序介绍OV5640 自动对焦介绍 网络摄像头实验硬件设计软件设计下载验证 网络摄像头ATK-MC2640实验ATK-MC2640 简介SCCB 介绍OV2640 行像素输出时序介绍OV2640 帧时序介绍 网络摄像头实验硬件设计软件设计下载验证 lwIP 内存管理内存的简介动态内存堆管理策略动态内存池管理策略使用C 库管理内存策略 lwIP 网络接口管理(管理不同网卡)网络接口结构netif lwIP 网络数据包管理TCP/IP 协议各层间的操作lwIP 的线程简介网络数据包 pbuf 结构网络接口接收数据lwIP 超时处理tcpip_thread协议栈线程lwIP 中的消息数据包消息(tcpip_msg)API 消息 RAW 编程接口UDP 实验UDP 协议简介UDP 报文的数据结构UDP 报文接收 RAW 的UDP 接口简介RAW 的UDP 实验硬件设计软件设计下载验证 RAW 编程接口TCP 服务器实验RAW 编程接口TCP 简介RAW 接口的TCP 实验硬件设计软件设计下载验证 RAW 编程接口Web Server 实验Web Server 文件以及相关技术简介Web Server 实验硬件设计软件设计下载验证 NETCONN 编程接口简介netbuf 数据缓冲区netconn 连接结构netconn 编程API 函数 NETCONN 编程接口UDP 实验NETCONN 实现UDPNETCONN 接口的UDP 实验硬件设计软件设计下载验证 NETCONN 编程接口TCP 服务器实验NETCONN 实现TCP 服务器步骤NETCONN 接口的TCPServer 实验硬件设计软件设计下载验证 Socket 编程接口简介Socket 编程接口简介Socket API 函数 Socket 编程接口UDP 实验Socket 编程UDP 连接流程Socket 接口的UDP 实验下载验证 Socket 编程接口TCP 服务器实验Socket 编程TCP 服务器流程Socket 接口的TCPServer 实验硬件设计软件设计下载验证 NTP 协议实验 NTPNetwork Time Protocol网络时间协议基于UDP用于网络时间同步的协议使网 络中的计算机时钟同步到UTC再配合各个时区的偏移调整就能实现精准同步对时功能。本 章在开发板上使用UDP 协议连接阿里云的NTP 服务器向这个服务器发送NTP 报文来获取 实时时间。 NTP 简介 NTP 服务器Network Time ProtocolNTP是用来使计算机时间同步化的一种协议它 可以使计算机对其服务器或时钟源如石英钟GPS 等等做同步化它可以提供高精准度 的时间校正LAN 上与标准间差小于1 毫秒WAN 上几十毫秒且可介由加密确认的方式 来防止恶毒的协议攻击。时间按NTP 服务器的等级传播。按照离外部UTC 源的远近把所有服 务器归入不同的Stratum层中。 NTP 数据报文格式如下图所示。 NTP 数据报文格式的各个字段的作用如下表所示 从上表可知NTP 报文的字段非常多这些字段并不是每一个都必须设置的请大家根 据项目的需要来构建NTP 请求报文。下面笔者使用网络调式助手制作一个简单的NTP 实验 如下图所示 图23.1.2 获取阿里云NTP 数据 上图中笔者使用网络调试助手以UDP 协议连接阿里云NTP 服务器接着在发送框上填 入NTP 请求报文发送完成之后网络调试助手接收到一段数据这里我们只取第40 位到43 位的十六进制数值该数值就是当前时间的总秒数。我们把总秒数转换成十进制并且在在线 转换器https://tool.lu/timestamp/上计算当前时间如下图所示 从上面的内容可知我们知道获取NTP 实时时间需要哪些步骤了这些步骤如下所示 ①以UDP 协议连接阿里云NTP 服务器。 ②发送NTP 报文到阿里云NTP 服务器。 ③获取阿里云NTP 服务器返回的数据取第40 位到43 位的十六进制数值。 ④把40 位到43 位的十六进制数值转成十进制。 ⑤把十进制数值减去1900-1970 的时间差2208988800 秒。 ⑥数值转成年月日时分秒。 NTP 实验 硬件设计 例程功能 使用UDP 协议连接阿里云的NTP 服务器并周期发送NTP 请求报文发送完成之后对 阿里云NTP 服务器返回的数据进行解析把它转换成实时时间信息。 该实验的实验工程请参考《lwIP 例程13 lwIP_ntp 实验》和《lwIP 例程14 lwIP_sntp 实 验》。 软件设计 23.2.2.1 程序流程图 本实验的程序流程图如下图所示。 程序解析 为了描述NTP 报文结构的字段笔者在lwip_demo.h 文件下定义了NPTformat 结构体 它用来描述NTP 报文结构体的各个字段该结构体如下所示 typedef struct _NPTformat {char version; /* 版本号*/char leap; /* 时钟同步*/char mode; /* 模式*/char stratum; /* 系统时钟的层数*/char poll; /* 更新间隔*/signed char precision; /* 精密度*/unsigned int rootdelay; /* 本地到主参考时钟源的往返时间*/unsigned int rootdisp; /* 统时钟相对于主参考时钟的最大误差*/char refid; /* 参考识别码*/unsigned long long reftime; /* 参考时间*/unsigned long long org; /* 开始的时间戳*/unsigned long long rec; /* 收到的时间戳*/unsigned long long xmt; /* 传输时间戳*/ } NPTformat; 该结构体的成员变量与表23.1.1 的NTP 报文结构体的字段是一一对应的。 打开lwip_demo.c 文件在此文件下定义了四个函数这些函数的作用如下表所示 (1) lwip_ntp_client_init 函数 此函数用来构建NTP 请求报文通过设置NPTformat 结构体的成员变量来描述NTP 报文 的字段信息构建完成之后把该报文存储在缓冲区当中。构建NTP 报文的源码如下所示 /***brief 初始化NTP Client信息*param 无*retval 无*/ void lwip_ntp_client_init(void) {uint8_t flag;g_ntpformat.leap 0; /* 时钟同步*/g_ntpformat.version 3; /* 版本号*/g_ntpformat.mode 3; /* 模式*/g_ntpformat.stratum 0; /* 系统时钟的层数*/g_ntpformat.poll 0; /* 更新间隔*/g_ntpformat.precision 0; /* 精密度*/g_ntpformat.rootdelay 0; /* 本地到主参考时钟源的往返时间*/g_ntpformat.rootdisp 0; /* 统时钟相对于主参考时钟的最大误差*/g_ntpformat.refid 0; /* 参考识别码*/g_ntpformat.reftime 0; /* 参考时间*/g_ntpformat.org 0; /* 开始的时间戳*/g_ntpformat.rec 0; /* 收到的时间戳*/g_ntpformat.xmt 0; /* 传输时间戳*/flag (g_ntpformat.version 3) g_ntpformat.mode;memcpy(g_ntp_message, (void const *)(flag), 1); } 可以看到笔者只设置NTP 报文的版本和模式字段其他字段我们设置为0。 (2) lwip_get_seconds_from_ntp_server 函数 此函数用来获取NTP 服务器返回的数据从这个数据截取40~43 位的数值并且强制转 换成十进制数值最后递交给其他函数处理。 /***brief 从NTP服务器获取时间*param buf存放缓存*param idx定义存放数据起始位置*retval 无*/ void lwip_get_seconds_from_ntp_server(uint8_t *buf, uint16_t idx) {unsigned long long atk_seconds 0;uint8_t i 0;for (i 0; i 4; i) /* 获取40~43位的数据*/{/* 把40~43位转成16进制再转成十进制*/atk_seconds (atk_seconds 8) | buf[idx i];}/* 减去减去1900-1970的时间差2208988800秒*/atk_seconds - NTP_TIMESTAMP_DELTA;lwip_calc_date_time(atk_seconds); /* 由UTC时间计算日期*/ } 调用此函数时该函数的idx 形参为40经过for 语句的作用可在数据中截取40~43 位 的数值截取完成之后强制转换成十进制并减去1900-1970 的时间差最后由lwip_calc_date_t ime 函数计算时间。 (3) lwip_calc_date_time 函数 此函数是把总秒数转换成时间信息该函数的源码如下所示 /***brief 计算日期时间*param seconds UTC 世界标准时间*retval 无*/ void lwip_calc_date_time(unsigned long long time) {unsigned int Pass4year;int hours_per_year;if (time 0){time 0;}nowdate.second (int)(time % 60); /* 取秒时间*/time / 60;nowdate.minute (int)(time % 60); /* 取分钟时间*/time / 60;nowdate.hour (int)(time % 24); /* 小时数*//* 取过去多少个四年每四年有1461*24 小时*/Pass4year ((unsigned int)time / (1461L * 24L));nowdate.year (Pass4year 2) 1970; /* 计算年份*/time % 1461 * 24; /* 四年中剩下的小时数*/for (;;) /* 校正闰年影响的年份计算一年中剩下的小时数*/{hours_per_year 365 * 24; /* 一年的小时数*/if ((nowdate.year 3) 0) /* 判断闰年*/{hours_per_year 24; /* 是闰年一年则多24小时即一天*/}if (time hours_per_year){break;}nowdate.year;time - hours_per_year;}time / 24; /* 一年中剩下的天数*/time; /* 假定为闰年*/if ((nowdate.year 3) 0) /* 校正闰年的误差计算月份日期*/{if (time 60){time--;}else{if (time 60){nowdate.month 1;nowdate.day 29;return;}}}/* 计算月日*/for (nowdate.month 0; Days[nowdate.month] time; nowdate.month){time - Days[nowdate.month];}nowdate.day (int)(time);return; } 总秒数经过算法的处理计算得出的年、月、‘时、分和秒都保存在DateTime 结构体当中。 (40) lwip_demo 函数 此函数调用lwIP 相关的API 接口以UDP 协议连接阿里云NTP 服务器连接完成之后 开启定时器定时发送NTP 请求报文最后处理NTP 服务器返回的数据。 /*** brief lwip_demo程序入口* param 无* retval 无*/ static void lwip_demo(void) {err_t err;static struct netconn *udpconn;static struct netbuf *recvbuf;static struct netbuf *sentbuf;ip_addr_t destipaddr;uint32_t data_len 0;struct pbuf *q;atk_ntp_client_init();/* 第一步创建udp控制块*/udpconn netconn_new(NETCONN_UDP);/* 定义接收超时时间*/udpconn-recv_timeout 10;if (udpconn ! NULL) /* 判断创建控制块释放成功*/{/* 第二步绑定控制块、本地IP和端口*/err netconn_bind(udpconn, IP_ADDR_ANY, NTP_DEMO_PORT);/* 域名解析*/netconn_gethostbyname((char *)(HOST_NAME), (destipaddr));/* 第三步连接或者建立对话框*/netconn_connect(udpconn, destipaddr, NTP_DEMO_PORT); /* 连接到远端主机*/if (err ERR_OK) /* 绑定完成*/{while (1){sentbuf netbuf_new();netbuf_alloc(sentbuf, 48);memcpy(sentbuf-p-payload, (void *)ntp_message,sizeof(ntp_message));err netconn_send(udpconn, sentbuf);if (err ! ERR_OK){printf(发送失败\r\n);netbuf_delete(sentbuf); /* 删除buf */}netbuf_delete(sentbuf); /* 删除buf *//* 第五步接收数据*/netconn_recv(udpconn, recvbuf);vTaskDelay(1000); /* 延时1s */if (recvbuf ! NULL) /* 接收到数据*/{/* 数据接收缓冲区清零*/memset(ntp_demo_recvbuf, 0, NTP_DEMO_RX_BUFSIZE);/* 遍历完整个pbuf链表*/for (q recvbuf-p; q ! NULL; q q-next){/* 判断要拷贝到UDP_DEMO_RX_BUFSIZE中的数据是否大于UDP_DEMO_RX_BUFSIZE的剩余空间如果大于的话就只拷贝UDP_DEMO_RX_BUFSIZE中剩余长度的数据否则的话就拷贝所有的数据*/if (q-len (NTP_DEMO_RX_BUFSIZE - data_len))/* 拷贝数据*/memcpy(ntp_demo_recvbuf data_len, q-payload,(NTP_DEMO_RX_BUFSIZE - data_len));elsememcpy(ntp_demo_recvbuf data_len,q-payload, q-len);data_len q-len;/* 超出TCP客户端接收数组,跳出*/if (data_len NTP_DEMO_RX_BUFSIZE)break;}data_len 0; /* 复制完成后data_len要清零*//*从NTP服务器获取时间*/atk_get_seconds_from_ntp_server(ntp_demo_recvbuf, 40);printf(北京时间%02d-%02d-%02d %02d:%02d:%02d\r\n,nowdate.year,nowdate.month 1,nowdate.day,nowdate.hour 8,nowdate.minute,nowdate.second);sprintf((char *)lwip_time_buf,Beijing time:%02d-%02d-%02d %02d:%02d:%02d,nowdate.year,nowdate.month 1,nowdate.day,nowdate.hour 8,nowdate.minute,nowdate.second);lcd_show_string(5, 170, lcddev.width, 16, 16,(char *)lwip_time_buf, RED);netbuf_delete(recvbuf); /* 删除buf */}elsevTaskDelay(5); /* 延时5ms */}}elseprintf(NTP绑定失败\r\n);}elseprintf(NTP连接创建失败\r\n); } 下载验证 编译代码并下载到开发板中打开串口调式助手查看当前时间如下图所示。 lwIP 测试网速 我们为什么测试网速呢原因很简单在我们开发时候有一些特殊的原因导致掉包、堵 塞、延迟抖动等情况一般都是发送和接收速率的问题如果网速偏低或者达不到PHY 芯片 的最大网速则开发过程中会遇到很多的问题。 JPerf 网络测速工具 JPerf 网络测速工具是一个跨平台的网络性能测试工具它支持Win/Linux/Mac/Android/iO S 等多个平台它也可以测试最大TCP 和UDP 带宽性能具有多种参数和UDP 特性可以根 据需要调整可以报告带宽、延迟抖动和数据包丢失该软件下载地址是https://iperf.fr/iperf -download.php。 下载完成之后打开该软件可以看到该软件划分为几个区域这些区域的作用如下所示 Iperf 命令行无法直接输入 服务端设置 (1) Listen Port 监听端口。 (2) client limit:客户端限制仅允许指定客户端连接。 (3) Num Connections最大允许连接的数量为0 不限制。 客户端设置 (1) Server address 表示服务器地址。 (2) Port 表示端口。 (3) Parallel Streams 表示并发流。 应用层设置 (1) Enable Compatibilitu Mode 兼容旧版本当server 端和client 端版本不一样时使用。 (2) Transmit 设置测试模式传输字节总量大小10Bytes 或者按传输时间总长度10 秒。 (3) Dual 复选框勾上表示同时进行双向传输测试。 (4) Trade 复选框勾上表示单独进行双向传输测试先测c 到s 的带宽。 (5) Representative File 表示指定需要传输的文件。 (6) Print MSS 表示显示tcp 最大mtu 值。 传输层设置 (1) TCP 协议-设置缓冲区大小。 (2) TCP 协议-指定TCP 窗口大小。 (3) TCP 协议-设定TCP 数据包的最大mtu 值。 (4) TCP 协议-设定TCP 不延时。 (5) UDP 协议-设置UDP 最大带宽。 (6) UDP 协议-设置UDP 缓冲区。 (7) UDP 协议-UDP 包封装大小默认1470 byte。 IP 层设置: (1) TTL 表示指定ttl 值。 (2) Type of Service 表示服务类型Type of ServiceToS大小范围从0x10 (最小延迟) 到0x2 (最少费用)在使用802.11e 来控制服务质量的WLAN 中ToS 是映射在Wi-Fi 多媒体(WMM)存取范畴的。 网速显示窗口折线图的形式显示 网速相关数据输出窗口以文本的形式输出 开始和停止JPerf JPerf 网络实验 硬件设计 例程功能 本实验的目标是利用软件JPerf 测试PHY 的网速。 该实验的实验工程请参考《lwIP 例程15 lwIP 测试接收速度实验》。 软件设计 24.2.2.1 程序解析 测试开发板收发速度的代码很简单只需要移植lwip-2.1.2\src\apps\lwiperf 的文件到工程 中接着在lwip_demo.c 文件下添加以下源码如下所示 /* 报告状态*/ const char *report_type_str[] {TCP_DONE_SERVER, /* LWIPERF_TCP_DONE_SERVER*/TCP_DONE_CLIENT, /* LWIPERF_TCP_DONE_CLIENT*/TCP_ABORTED_LOCAL, /* LWIPERF_TCP_ABORTED_LOCAL */TCP_ABORTED_LOCAL_DATAERROR, /*LWIPERF_TCP_ABORTED_LOCAL_DATAERROR*/TCP_ABORTED_LOCAL_TXERROR, /* LWIPERF_TCP_ABORTED_LOCAL_TXERROR */TCP_ABORTED_REMOTE, /* LWIPERF_TCP_ABORTED_REMOTE */UDP_STARTED, /* LWIPERF_UDP_STARTED,*/UDP_DONE, /* LWIPERF_UDP_DONE */UDP_ABORTED_LOCAL, /* LWIPERF_UDP_ABORTED_LOCAL*/UDP_ABORTED_REMOTE /* LWIPERF_UDP_ABORTED_REMOTE */ }; /* 当测试结束以后会调用此函数此函数用来报告测试结果*/ static void lwiperf_report(void *arg,enum lwiperf_report_type report_type,const ip_addr_t *local_addr,u16_t local_port,const ip_addr_t *remote_addr,u16_t remote_port,u32_t bytes_transferred,u32_t ms_duration,u32_t bandwidth_kbitpsec) {printf(-------------------------------------------------\r\n);if ((report_type (sizeof(report_type_str) / sizeof(report_type_str[0]))) local_addr remote_addr){printf( %s \r\n, report_type_str[report_type]);printf( Local address : %u.%u.%u.%u , ((u8_t *)local_addr)[0],((u8_t *)local_addr)[1],((u8_t *)local_addr)[2],((u8_t *)local_addr)[3]);printf( Port %d \r\n, local_port);printf( Remote address : %u.%u.%u.%u , ((u8_t *)remote_addr)[0],((u8_t *)remote_addr)[1],((u8_t *)remote_addr)[2],((u8_t *)remote_addr)[3]);printf( Port %d \r\n, remote_port);printf( Bytes Transferred %d \r\n, bytes_transferred);printf( Duration (ms) %d \r\n, ms_duration);printf( Bandwidth (kbitpsec) %d \r\n, bandwidth_kbitpsec);}else{printf( IPERF Report error\r\n);} } /*** brief lwip_demo实验入口* param 无* retval 无*/ void lwip_demo(void) {uint8_t t 0;if (lwiperf_start_tcp_server_default(lwiperf_report, NULL)){printf(\r\n************************************************\r\n);printf( IPERF Server example\r\n);printf(************************************************\r\n);printf( IPv4 Address : %u.%u.%u.%u\r\n, lwipdev.ip[0],lwipdev.ip[1],lwipdev.ip[2],lwipdev.ip[3]);printf( IPv4 Subnet mask : %u.%u.%u.%u\r\n, lwipdev.netmask[0],lwipdev.netmask[1],lwipdev.netmask[2],lwipdev.netmask[3]);printf( IPv4 Gateway : %u.%u.%u.%u\r\n, lwipdev.gateway[0],lwipdev.gateway[1],lwipdev.gateway[2],lwipdev.gateway[3]);printf(************************************************\r\n);}else{printf(IPERF initialization failed!\r\n);}while (1){vTaskDelay(5);} } 测试网速的相关原理这里笔者不会讲解有兴趣的小伙伴可以看一下lwiperf.c/.h 文件。 下载验证 编译程序并下载到开发板上双击jperf.bat填写IP 地址与端口号如下所示 图24.2.3.1 测试网速的IP 地址和端口号 图24.2.3.2 网速波形图 可以看到我们的网速接近95M虽然离100M 有一点点差距但是速率受很多因素影响。 提高网速的速率可设置以下几个配置项如下所示 /* 堆内存的大小如果需要更大的堆内存那么设置高一点*/ #define MEM_SIZE (30 * 1024) /* MEMP_NUM_PBUF: 设置内存池的数量*/ #define MEMP_NUM_PBUF 25 /* MEMP_NUM_UDP_PCB: UDP协议控制块的数量. */ #define MEMP_NUM_UDP_PCB 4 /* MEMP_NUM_TCP_PCB: TCP的数量. */ #define MEMP_NUM_TCP_PCB 4 /* MEMP_NUM_TCP_PCB_LISTEN: 监听TCP的数量. */ #define MEMP_NUM_TCP_PCB_LISTEN 2 /* MEMP_NUM_TCP_SEG: 同时排队的TCP的数量段. */ #define MEMP_NUM_TCP_SEG 150 /* MEMP_NUM_SYS_TIMEOUT: 超时模拟活动的数量. */ #define MEMP_NUM_SYS_TIMEOUT 6 /* ---------- Pbuf选项---------- */ /* PBUF_POOL 内存池中每个内存块大小*/ #define PBUF_POOL_SIZE 20 /* PBUF_POOL_BUFSIZE: pbuf池中每个pbuf的大小. */ #define PBUF_POOL_BUFSIZE LWIP_MEM_ALIGN_SIZE(TCP_MSS 40 PBUF_LINK_ENCAPSULATION_HLEN PBUF_LINK_HLEN) /* TCP接收窗口*/ #define TCP_WND (20 * TCP_MSS)HTTP 服务器实验 本章实验我们在开发板上搭建一个HTTP 服务器通过浏览器去访问我们的开发板这个 实验和第十四章的实验不同的是该实验使用字符串的形式描述网页数据而第十四章的实验使 用的是网页数组形式搭建HTTP 服务器。本实验参考contrib-2.1.0\apps\httpserver 路径下的 httpserver-netconn.c/.h 下的例程。 HTTP 协议简介 HTTP 协议是Hyper Text Transfer Protocol超文本传输协议的缩写,是用于从万维网 WWW:World Wide Web 服务器传输超文本到本地浏览器的传送协议。HTTP 是一种无状态 协议即服务器不保留与客户交易时的任何状态。这就大大减轻了服务器记忆负担从而保持 较快的响应速度。HTTP 是一种面向对象的协议。允许传送任意类型的数据对象。它通过数据 类型和长度来标识所传送的数据内容和大小并允许对数据进行压缩传送。当用户在一个 HTML 文档中定义了一个超文本链后浏览器将通过TCP/IP 协议与指定的服务器建立连接 如下所示 图25.1.1 HTTP 协议交互 HTTP定义了与服务器交互的不同方法其最基本的方法是GET、PORT 和HEAD。如 下图所示。 ①GET从服务端获取数据。 ②PORT向服务器传送数据。 ③HEAD检测一个对象是否存在。 浏览器Client (Server) PORT提交更新和控制 LED/BEEP 图25.1.2 HTTP 基本方法使用 可以知道“GET”请求用来获取服务器的数据而“PORT”请求是向服务器转送数据。 HTTP 服务器实验 硬件设计 例程功能 在开发板上搭建一个HTTP 服务器并实时控制开发板上的LED1 和蜂鸣器。 该实验的实验工程请参考《lwIP 例程16 lwIP_HTTPS 实验》。 25.2.2 软件设计 25.2.2.1 程序流程图 本实验流程图如下图所示 程序解析 本实验重点看lwip_demo.c 文件该文件定义了三个函数如下表所示 lwip_demo 函数用来配置网络环境这里笔者把开发板设置为TCP 服务器其端口号为 80。 /*** brief lwip_demo程序入口* param 无* retval 无*/ void lwip_demo(void) {struct netconn *conn, *newconn;err_t err;/* 创建一个新的TCP连接句柄*//* 使用默认IP地址绑定到端口80 (HTTP) */conn netconn_new(NETCONN_TCP);netconn_bind(conn, IP_ADDR_ANY, 80);/* 将连接置于侦听状态*/netconn_listen(conn);do{err netconn_accept(conn, newconn);if (err ERR_OK){http_server_netconn_serve(newconn);netconn_delete(newconn);}} while (err ERR_OK);netconn_close(conn);netconn_delete(conn); } 连接完成之后调用http_server_netconn_serve 函数实现本章节的功能。 lwip_server_netconn_serve 函数源码如下所示 static void lwip_server_netconn_serve(struct netconn *conn) {struct netbuf *inbuf;char *buf;u16_t buflen;err_t err;char *ptemp;/* 从端口读取数据如果那里还没有数据则阻塞。我们假设请求(我们关心的部分)在一个netbuf中*/err netconn_recv(conn, inbuf);if (err ERR_OK){netbuf_data(inbuf, (void **)buf, buflen);/* 这是一个HTTP GET命令吗?只检查前5个字符因为GET还有其他格式我们保持简单)*/if (buflen 5 buf[0] G buf[1] E buf[2] T buf[3] buf[4] /){start_html:/* 发送HTML标题从大小中减去1因为我们没有在字符串中发送\0NETCONN_NOCOPY:我们的数据是常量静态的所以不需要复制它*/netconn_write(conn, http_html_hdr, sizeof(http_html_hdr) - 1,NETCONN_NOCOPY);/* 发送我们的HTML页面*/netconn_write(conn, http_index_html, sizeof(http_index_html) - 1,NETCONN_NOCOPY);}else if (buflen 8 buf[0] P buf[1] O buf[2] S buf[3] T){ptemp lwip_data_locate((char *)buf, led1);if (ptemp ! NULL){/* 查看led1的值。为1则灯亮为2则灭此值与HTML网页中设置有关*/if (*ptemp 1){/* 点亮LED1 */LED0(0);}else{/* 熄灭LED1 */LED1(1);}}/* 查看beep的值。为3则灯亮为4则灭此值与HTML网页中设置有关*/ptemp atk_data_locate((char *)buf, beep);if (ptemp ! NULL){if (*ptemp 3){/* 打开蜂鸣器*/BEEP(1);}else{/* 关闭蜂鸣器*/BEEP(0);}}goto start_html;}}/* 关闭连接(服务器在HTTP中关闭) */netconn_close(conn);/* 删除缓冲区(netconn_recv给我们所有权所以我们必须确保释放缓冲区) */netbuf_delete(inbuf); } 上述的源码很简单理解主要分为三步 ①当浏览器输入IP 地址并且回车确认时程序调用函数netconn_write 把网页数据发送 到浏览器当中。 ②当网页发送一个PORT 命令时程序调用函数lwip_data_locate 判断触发源判断完 成之后根据触发源来执行相应的动作。 ③程序执行goto 语句重新发送网页字符串到网页当中这个步骤相当于更新网页。 下载验证 编译程序并把程序下载到开发板中打开网页同时需要查看分配的IP 地址为多少接 着在浏览器上输入IP 地址如下图所示 网络摄像头ATK-MC5640实验 网络摄像头是传统摄像机与网络视频技术相结合的新一代产品除了具备一般传统摄像机 所有的图像捕捉功能外机内还内置了数字化压缩控制器和基于WEB 的操作系统使得视频 数据经压缩加密后通过局域网internet 或无线网络送至终端用户。而远端用户可在PC 上使 用标准的网络浏览器根据网络摄像机的IP 地址对网络摄像机进行访问实时监控目标现 场的情况并可对图像资料实时编辑和存储同时还可以控制摄像机的云台和镜头进行全方 位地监控。本章的实验是以网络调试助手作为客户端开发板作为服务器。服务器把摄像头处 理的数据使用网卡发送至服务器当中并且在服务器实时更新图像。 ATK-MC5640 简介 ATK-MC5640 模块通过2*9 的排针2.54mm 间距同外部相连接该模块可直接与正点 原子探索者STM32F407 开发板和正点原子MiniSTM32H750 开发板等开发板的CAMERA 摄像 头接口连接。正点原子的大部分开发板我们都提供了本模块相应的例程用户可以直接在这 些开发板上对模块进行测试。 ATK-MC5640 模块的外观如下图所示 图30.1.1 ATK-MC5640 模块实物图 ATK-MC5640 模块的原理图如下图所示 从上图可以看出ATK-MC5640 模块自带了有源晶振用于产生24MHz 的时钟作为 OV5640 传感器的XCLK 输入模块的闪光灯LED1 和LED2可由OV5640 的STROBE 脚 控制可编程控制或外部引脚控制只需焊接R2 或R3 的电阻进行切换控制同时模块 同时自带了稳压芯片用于提供OV5640 稳定的2.8V 和1.5V 工作电压。 ATK-MC5640 模块通过一个2*9 的排针P1同外部电路连接各引脚的详细描述如 下表所示 SCCB 简介 SCCBSerial Camera Control Bus串行摄像头控制总线是OmniVision 开发的一种总线 协议且广泛被应用于OV 系列图像传感器上。SCCB 协议与IIC 协议十分相似SCCB 协议 由两条信号线组成SIO_C类似IIC 协议的SCL和SIO_D类似IIC 协议的SDA。与IIC 协议一样SCCB 协议也有起始信号和停止信号只不过与IIC 协议不同的是IIC 协议在传输完1 字节数据后需要传输的接收方发送1 比特的确认位而SCCB 协议一次性要传输9 位 数据前8 位为读写的数据位第9 位在写周期为Don’t-Care 位在读周期为NA 位。这样一 次性传输的9 个位在SCCB 协议中被定义为一个相Phase。 在SCCB 协议中共包含了三种传输周期分别为3 相写传输三个相均由主机发出一般 用于主机写从机寄存器三个相分别从设备地址、寄存器地址、写入的数据、2 相写传输 两个相均由主机发出一般配合2 相读传输用与主机读从机寄存器值两个相分别为从设备 地址、寄存器地址和2 相读传输第一个相由主机发出第二个相由从机回应一般配合2 相写传输用于主机读从机寄存器值两个相分别为从设备地址、寄存器数据。 关于SCCB 协议的详细介绍请见《OmniVision Technologies Seril Camera Control Bus(SCCB) Specification.pdf》。 在OV5640 图像传感器的初始化阶段主机MCU 需要使用SCCB 协议配置OV5640 中大 量的寄存器有关OV5640 寄存器的介绍请见《OV5640_CSP3_DS_2.01_Ruisipusheng.pdf》 和《OV5640_camera_module_software_application_notes_1.3_Sonix.pdf》。 OV5640 DVP 接口说明 OV5640 支持数字视频接口DVP和MIPI 接口因为正点原子探索者STM32F407 和正 点原子MiniSTM32H750 等开发板的CANERA 接口使用的是DCMI 接口仅支持DVP 接口 因此OV5640 必须使用DVP 输出接口才能够连正点原子探索者STM32F407 和正点原子 MiniSTM32H750 等开发板使用。 OV5640 提供了一个10 位的DVP 接口支持8 位接发可通过程序设置DVP 以MSB 或LSB 输出ATK-MC5640 模块采用8 位DVP 连接的方式如下图所示 图30.1.1.1 ATK-MC56408 位DVP 连接方式 OV5640 窗口设置说明 OV5640 输出的图像与ISPImage Signal Processor输入窗口、预缩放窗口和数据输出窗 口的大小有关如下图所示 ISP 输入窗口ISP imput size 该窗口的大小允许用于设置整个传感器区域physical pixel size26231951的执行部分 也就是在传感器里面开窗X_ADDR_ST、Y_ADDR_ST、X_ADDR_END、Y_ADDR_END 开窗范围从00~2623*1951 都可以设置该窗口所设置的范围将输入ISP 进行处理。 ISP 输入窗口通过寄存器地址为0x3800~0x3807 的八个寄存器进行配置。 预缩放窗口pre-scaling size 该窗口允许用于在ISP 输入窗口的基础上再次设置想要用于缩放的窗口大小。该窗口仅在 ISP 输入窗口内进行X、Y 方向的偏移X_OFFSET、Y_OFFSET。 预缩放窗口通过寄存器地址为0x3808~0x380B 的四个寄存器进行配置。 数据输出窗口data output size 该窗口是OV5640 输出给外部的图像尺寸当数据输出窗口的宽高比例与预缩放窗口的宽 高比例不一致时输出的图像数据会变形只有当两者比例一致时输出图像的尺寸才不会变 形。 OV5640 行像素输出时序介绍 OV5640 图像传感器的数据输出通过D[9:0]是在PCLK、VSYNC、HREFHSYNV 的控制下进行的。行输出时序如下图所示 从上图可以看出图像数据在HREF 为高的时候输出当HREF 变高后每一个PCLK 时钟输出一个8 位或10 位的数据ATK-MC5640 模块采用8 位所以每个PCLK 输出1 个 字节图像数据且在RGB/YUV 输出格式下每个像素数据需要两个PCLK 时钟在Raw 输 出格式下每个像素数据需要一个PCLK 时钟。例如采用QSXGA 分辨率RGB565 格式输出 那么一个像素的信息由两个字节组成低字节在前高字节在后这样每行图像数据就需要 25922 个PCLK 时钟输出25922 个字节。 接下来以QSXGA 分辨率为例介绍帧输出的时序如下图所示 图30.1.4.2 OV5640 帧输出时序图 上图清楚的展示了OV5640 在QSXGA 分辨率下的数据输出。只需按照这个时序去读取 OV5640 的数据就可以得到图像数据。 OV5640 自动对焦介绍 OV5640 的自动对焦Auto Focus由其内置的微控制器完成并且VCMVoice Coil Motor音圈马达驱动器也集成在传感器内部。OV5640 内置微控制器的自动对焦控制固件 Firmware需要从外接的主控芯片下载。当固件运行后内置微处理器从OV5640 传感器自 动获取自动对焦所需的信息然后计算并驱动VCM 带动镜头达到正确的对焦位置。外接主控 芯片可以通过SCCB 协议控制OV5640 内置微处理器的各种功能。 OV5640 自动对焦相关的寄存器如下表所示 OV5640 内置处理器接收到自动对焦命令后会自动将CMD_MAIN 寄存器清零当命令执 行完成后则会将CMD_ACK 寄存器清零。 自动对焦过程 下载固件 OV5640 初始化完成后就可以下载自动对焦固件固件数据由厂家提供了其操作过 程就是通过SCCB 写OV5640 的寄存器自动对焦固件下载的起始地址为0x8000固件下载 完成后通过检查FW_STATUS 寄存器来判断固件下载状态当读取到FW_STATUS 寄存器 的值为0x70 时说明自动对焦固件下载完成。自动对焦后获取图像 OV5640 支持单次自动对焦和持续自动对焦通过CMD_MAIN 寄存器配置配置 OV5640 单次自动对焦的过程如下 a. 往CMD_MAIN 寄存器写0x03触发单次自动对焦。 b. 读取FW_STATUS 寄存器直至读到0x10说明已完成对焦。 c. 往CMD_MAIN 寄存器写0x06暂停自动对焦过程镜头将保持在对焦位置。 配置OV5640 持续自动对焦的过程如下 a. 往CMD_MAIN 寄存器写0x08释放VCM 至初始状态对焦到无穷远处。 b. 往CMD_MAIN 寄存器写0x04启动持续自动对焦。 c. 读取CMD_ACK 寄存器直至读到0x00说明命令执行完成。释放VCM 到初始状状态 通过往CMD_MAIN 寄存器写0x08即可释放VCM结束自动对焦。 网络摄像头实验 硬件设计 例程功能 在本实验中开发板主控芯片通过模拟SCCB 协议对ATK-MC5640 模块中的摄像头传感 器进行配置等通讯并通过DCMI 接口获取ATK-MC5640 模块输出的JPEG 图像数据然后 将获取到的图像数据实时的发往至正点原子自研的ATK-XCAM 软件。 该实验的实验工程请参考《lwIP 例程21 lwIP_网络摄像头实验MC5640》或者《lwIP 例程21 lwIP_网络摄像头实验MC5640 全帧输出》。 注探索者、DMF407 开发板没有本实验例程。 软件设计 30.2.2.1 程序流程图 本实验的程序流程图如下图所示 程序解析 相关ATK-MC5640 驱动文件介绍请参考《ATK-MC5640 模块使用说明》和《ATK- MC5640 模块用户手册》文档。 实验的测试代码为文件lwip_demo.c在工程下的Middlewares\lwip\lwip_app 路径中。测 试代码的入口函数为lwip_demo()具体的代码如下所示 /*** brief lwip_demo实验入口* param 无* retval 无*/ void lwip_demo(void) {err_t err;struct netconn *conn;static ip_addr_t ipaddr;uint8_t remot_addr[4];static u16_t port;uint8_t *p_jpeg_buf;uint32_t jpeg_len;uint32_t jpeg_index;uint32_t jpeg_start_index;uint32_t jpeg_end_index;conn netconn_new(NETCONN_TCP); /* 创建一个TCP链接*/netconn_bind(conn, IP_ADDR_ANY, 8088); /* 绑定端口8088号端口*/netconn_listen(conn); /* 进入监听模式*/while (1) /* 等待连接*/{err netconn_accept(conn, g_newconn); /* 接收连接请求*/if (err ERR_OK) /* 成功检测到连接*/{/* 获取远端IP地址和端口号*/netconn_getaddr(g_newconn, ipaddr, port, 0);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);lwip_camera_init();delay_ms(1000); /* 此延时一定要加*/while (1) /* 开始视频传输*/{p_jpeg_buf (uint8_t *)g_jpeg_buf;jpeg_len DEMO_JPEG_BUF_SIZE / (sizeof(uint32_t));memset((void *)g_jpeg_buf, 0, DEMO_JPEG_BUF_SIZE);/* 获取ATK-MC5640模块输出的一帧JPEG图像数据*/atk_mc5640_get_frame((uint32_t)g_jpeg_buf,ATK_MC5640_GET_TYPE_DTS_32B_INC, NULL);/* 获取JPEG图像数据起始位置*/for (jpeg_start_index UINT32_MAX, jpeg_index 0;jpeg_index DEMO_JPEG_BUF_SIZE - 1; jpeg_index){if ((p_jpeg_buf[jpeg_index] 0xFF) (p_jpeg_buf[jpeg_index 1] 0xD8)){jpeg_start_index jpeg_index;break;}}if (jpeg_start_index UINT32_MAX){continue;}/* 获取JPEG图像数据结束位置*/for (jpeg_end_index UINT32_MAX, jpeg_index jpeg_start_index;jpeg_index DEMO_JPEG_BUF_SIZE - 1; jpeg_index){if ((p_jpeg_buf[jpeg_index] 0xFF) (p_jpeg_buf[jpeg_index 1] 0xD9)){jpeg_end_index jpeg_index;break;}}if (jpeg_end_index UINT32_MAX){continue;}/* 获取JPEG图像数据的长度*/jpeg_len jpeg_end_index - jpeg_start_index (sizeof(uint32_t) 1);err netconn_write(g_newconn, g_jpeg_buf,DEMO_JPEG_BUF_SIZE, NETCONN_COPY); /* 发送数据*/if ((err ERR_CLSD) || (err ERR_RST)){myfree(SRAMCCM, g_jpeg_buf);netconn_close(g_newconn);netconn_delete(g_newconn);break;}vTaskDelay(2); /* 延时2ms */}}} } 上面的代码还是比较简单的首先把开发板配置为TCP 服务器配置完成且连接成功之 后将ATK-MC5640 模块输出的JPEG 图像数据读取至缓冲空间由于JPEG 图像数据的大小是 不确定的因此首先就要计算出JPEG 图像数据的大小然后将JPEG 图像数据通过网络输出 至ATK-XCAM 上位机进行显示。 下载验证 将ATK-MC5640 模块按照前面介绍的连接方式与开发板连接同时将开发板与上位机通 讯的串口连接至PC并将实验代码编译烧录至开发板中如果DHCP 服务器分配完成那么 串口调试助手显示如下信息 图30.2.3.1 串口调试助手显示内容 接下来如果ATK-MC5640 模块初始化成功则会在上位机上显示ATK-MC5640 模块输 出的JPEG 图像如下图所示 图30.2.3.2 网络调试助手显示内容 网络摄像头ATK-MC2640实验 本章我们来实现一下ATK-MC2640 模块的网络摄像头实验。 ATK-MC2640 简介 ATK-MC2640 模块通过2*9 的排针2.54mm 间距同外部相连接该模块可直接与正点 原子战舰STM32F103 开发板、正点原子探索者STM32F407 开发板和正点原子 MiniSTM32H750 开发板等开发板的CAMERA 摄像头接口连接。正点原子的大部分开发板 我们都提供了本模块相应的例程用户可以直接在这些开发板上对模块进行测试。 ATK-MC2640 模块的外观如下图所示 图31.1.1.1 ATK-MC2640 模块实物图 ATK-MC2640 模块的原理图如下图所示 图31.1.1.2 ATK-MC2640 模块原理图 从上图可以看出ATK-MC2640 模块自带了有源晶振用于产生24MHz 的时钟作为OV2640 传感器的XCLK 输入模块的闪光灯LED1 和LED2可由OV2640 的STROBE 脚 控制可编程控制或外部引脚控制只需焊接R2 或R3 的电阻进行切换控制同时模块 同时自带了稳压芯片用于提供OV2640 稳定的2.8V 和1.3V 工作电压 ATK-MC2640 模块通过一个2*9 的排针P1同外部电路连接各引脚的详细描述如 下表所示 SCCB 介绍 SCCB 协议相关知识请读者查看30.1.1 小节内容。 在OV2640 图像传感器的初始化阶段主机MCU 需要使用SCCB 协议配置OV2640 中大 量的寄存器有关OV2640 寄存器的介绍请见《OV2640_DS(1.6).pdf》和《OV2640 Software Application Notes 1.03.pdf》。 OV2640 行像素输出时序介绍 OV2640 图像传感器的数据输出是在行参考信号的像素时钟的控制下有序输出的默 认的行像素输出时序如下图所示 图31.1.2.1 OV2640 图像传感器行像素输出时序图 如上图所示当行参考信号HREF为高电平时表示数据端口的数据有效此时每 输出一个像素时钟PCLK就输出一个数据8bit 或10bit。数据在PCLK 的下降沿更新 所以外接主控须在PCLK 的上升沿读取数据。 注意图中的tP表示像素周期像素周期可能等于一个像素时钟周期或两个像素时钟周 期。在RGB/YUV 输出格式下每个像素周期等于两个像素时钟周期在RawRGB 输出格式 下每个像素周期等于一个像素时钟周期。 以RGB565 的输出格式为例一个像素周期等于两个像素时钟周期每一个像素需要用 两个字节表示低字节在前高字节在后那么如果采用UXGA 分辨率输出图像数据那么 每输出一行图像数据就需要1600*2 个像素时钟。 当使用JPEG 格式输出图像数据时输出的图像数据是经过压缩的数据这里与普通的行 橡树输出时序略有不同普通的行像素输出时行参考信号是连续的也就是在一行数据输出 的过程中行参考信号是一直保持高电平的而JPEG 格式输出图像数据时行参考信号并不 是连续的有可能在一行图像数据输出的过程中多次出现低电平但这并不影响数据的读取 只需判断行参考信号为高电平的时候再读取数据就可以了。JPEG 格式输出的图像数据不 存在高低字节的概念只需要从头到尾将所有的数据读取保存下来就可以完成一次JPEG 数据采集。 注意PCLK 的频率可达36MHz所以外接主控在读取数据的时候必须速度够快才可 以否则就可能出现数据丢失。对于速度不够快的MCU我们可以通过设置OV2640 的寄存 器0xD3 和0x11设置PCLK 和时钟的分频来降低PCLK 速度从而使得低速外接主控也 可以读取OV2640 的数据。不过这样会降低帧率。 OV2640 帧时序介绍 一帧图像数据实际上就是由多行像素输出的数据组成的。这里以UXGA 的帧时序为例 进行介绍UXGA 的帧时序如下图所示 图31.1.3.1 UXGA 帧时序图 如上图所示tLINE为行输出时间tP为像素周期VSYNC 为帧同步信号每一个VSYNC 脉冲表示一个新帧的开始而整个帧周期内由1200 次行像素Row输出每一行为 1600 个像素这样得到的数据正好为1600*1200 的分辨率图像数据。 HSYNC 为行同步信号用于同步行输出数据不过ATK-MC2640 模块并没有引出该信号 因此使用HREF 做同步即可。 网络摄像头实验 硬件设计 例程功能 本实验与上一章节的实验类似只不过本实验配置ATK-MC2640 模块输出JPEG 图像数 据然后将通过DCMI 接口读取到的JPEG 图像数据通过网络输出至正点原子自研的ATK- XCAM 软件显示。 该实验的实验工程请参考《lwIP 例程21 lwIP_网络摄像头实验MC5640》。 注DMF407 开发板没有本实验例程。 软件设计 31.2.2.1 程序流程图 本实验的程序流程图如下图所示 程序解析 相关ATK-MC2640 驱动文件介绍请参考《ATK-MC2640 模块使用说明》和《ATK-MC2640 模块用户手册》文档。 实验的测试代码为文件lwip_demo.c在工程下的Middlewares\lwip\lwip_app 路径中。测 试代码的入口函数为lwip_demo()具体的代码如下所示 void lwip_demo(void) {struct netconn *conn;static ip_addr_t ipaddr;uint8_t remot_addr[4];static u16_t port;uint32_t *jpeg_buf;uint32_t jpeg_len;conn netconn_new(NETCONN_TCP); /* 创建一个TCP链接*/netconn_bind(conn, IP_ADDR_ANY, 8088); /* 绑定端口8088号端口*/netconn_listen(conn); /* 进入监听模式*/while (1){err netconn_accept(conn, g_newconn); /* 接收连接请求*/if (err ERR_OK){/* 初始化ATK-MC2640模块*/lwip_camera_init();/* 为JPEG缓存空间申请内存*/jpeg_buf mymalloc(SRAMIN, DEMO_JPEG_BUF_SIZE);delay_ms(1000);while (1){jpeg_len DEMO_JPEG_BUF_SIZE / (sizeof(uint32_t));memset((void *)jpeg_buf, 0, DEMO_JPEG_BUF_SIZE);/* 获取ATK-MC2640模块输出的一帧JPEG图像数据*/atk_mc2640_get_frame((uint32_t)jpeg_buf,ATK_MC2640_GET_TYPE_DTS_32B_INC, NULL);/* 获取JPEG图像数据的长度*/while (jpeg_len 0){if (jpeg_buf[jpeg_len - 1] ! 0){break;}jpeg_len--;}jpeg_len * sizeof(uint32_t);/* 发送JPEG图像数据*/err netconn_write(g_newconn, jpeg_buf, jpeg_len, NETCONN_COPY);if ((err ERR_CLSD) || (err ERR_RST)){myfree(SRAMIN, (void *)jpeg_buf);netconn_close(g_newconn);netconn_delete(g_newconn);break;}vTaskDelay(2); /* 延时2ms */}}} } 上面的代码还是比较简单的首先开发板配置为TCP 服务器模式配置完成且连接成功 之后调用函数atk_mc2640_get_frame 获取ATK-MC2640 模块输出的一帧JPEG 图像数据同时 调用netconn_write 函数把这一帧的图像数据传输至ATK-XCAM 上位机显示。 下载验证 将ATK-MC2640 模块按照前面介绍的连接方式与开发板连接同时将开发板与上位机通 讯的串口连接至PC并将实验代码编译烧录至开发板中如果DHCP 服务器分配完成那么 串口调试助手显示如下信息 图31.2.3.1 串口调试助手显示内容 接下来如果ATK-MC2640 模块初始化成功则会在上位机上显示ATK-MC2640 模块输 出的JPEG 图像如下图所示 图31.2.3.2 网络调试助手显示内容 lwIP 内存管理 对于嵌入式系统而言内存管理始终是最重要的一环内存管理的选择将从根本上决定内 存分配和回收效率最终决定系统的性能。lwIP 为使用者提供了两种简单却又高效的内存管 理机制它们分别为动态内存池管理和动态内存堆管理。 内存的简介 在lwIP 中内存分配策略有两种一种是动态内存池管理策略另一种是动态内存堆 管理策略它们在lwIP 中起到以长补短的作用lwIP 内核根据不同的场景而选择不同的分配 方式使系统的内存开销和分配效率大大的提高。说到内存分配我们不得不想起C 语言也是 有提供内存分配它是使用库中的malloc 和free 进行内存分配当然lwIP 也是支持这种分配 方式的但是lwIP 不建议使用C 标准库内存分配策略主要原因笔者留到本章的4.5 小节来 讲解。 lwIP 的宏配置及内存管理 在lwIP 中内存的选择需要以下几个宏定义的值来决定用户可以根据宏值来判断lwIP 使 用那种内存管理策略如下表所示 注lwIP 内存堆管理策略和C 标准库管理策略只能选其一若MEM_LIBC_MALLOC 为 0则lwIP 内核选择内存堆管理策略。 动态内存堆管理策略 动态内存堆也叫可变长分配方式这种可变长的内存块分配在很多系统中被用到系统本 身就是一个很大的内存堆随着系统的运行不断的申请和释放内存造成了系统的内存块的大 小和数量随之改变严重一点可能造成内存碎片。lwIP 动态内存堆策略采用First Fit首次拟 合内存管理算法。该算法倾向于优先利用内存中低址部分的空闲分区从而保留了高址部分 的大空闲区这为以后到达的大作业分配大的内存空间创造了条件但是缺点也是明显的因 为首次拟合First Fit算法是从低地址不断被划分的所以系统会留下许多难以利用的且很 小的空闲分区我们称为内存碎片。每次申请内存时系统每次查找都是从低地址部分开始的 这无疑又会增加查找可用空闲分区时的时间。 下面笔者分几个部分解析lwIP 内存堆算法的实现代码该算法由mem.c 和mem.h 文件组 成其中mem.c 尤为重要它实现了lwIP 内存堆的分配和释放原理。 (1) 内存堆的结构体 管理内存块的结构体如下源码所示 struct mem {mem_size_t next; /* 保存下一个内存块的索引*/mem_size_t prev; /* 保存前一个内存块的索引*/u8_t used; /* 此内存快是否被用。1使用、0 未使用*/ };可以看出这个结构体只定义了三个成员变量其中next、prev 变量用来保存下一个和前 一个内存块的索引而used 变量用来声明被管理的内存块是否可用。 (2) 内存堆的对齐及最小配置值 #ifndef MIN_SIZE #define MIN_SIZE 12 #endif /* MIN_SIZE */ /* 最小大小做对齐处理后面均用对齐后的该宏值*/ #define MIN_SIZE_ALIGNED LWIP_MEM_ALIGN_SIZE(MIN_SIZE) /* 内存块头大小做对齐处理后面均用对齐后的该宏值*/ #define SIZEOF_STRUCT_MEM LWIP_MEM_ALIGN_SIZE(sizeof(struct mem)) /* 用户定义的堆大小做对齐处理后面均用对齐后的该宏值*/ #define MEM_SIZE_ALIGNED LWIP_MEM_ALIGN_SIZE(MEM_SIZE)lwIP 内核为了有效防止内存碎片它定义了最小分配大小MIN_SIZE若用户申请的内存 小于最小分配内存则系统分配MIN_SIZE 大小的内存资源。往下的宏定义是对内存大小进 行4 字节对齐。注内存对齐的作用1平台原因不是全部的硬件平台都能访问随意地址 上的随意类型数据的某些硬件平台仅仅能在某些地址处取某些特定类型的数据否则抛出硬 件异常。2性能原因经过内存对齐后CPU 的内存访问速度大大提升。 (3) 定义内存堆的空间 #ifndef LWIP_RAM_HEAP_POINTER /*定义堆内存空间*/ LWIP_DECLARE_MEMORY_ALIGNED(ram_heap, MEM_SIZE_ALIGNED (2U * SIZEOF_STRUCT_MEM)); #define LWIP_RAM_HEAP_POINTER ram_heap #endif无论是内存堆还是内存池它们都是对一个大数组进行操作上述的宏定义就是指向一个 名为ram_heap 数组该数组的大小为MEM_SIZE_ALIGNED (2U*SIZEOF_STRUCT_MEM) lwIP 内存堆申请的内存就是从这个数组分配得来的。 (4) 操作内存堆的变量 /* 指向对齐后的内存堆的地址*/ static u8_t *ram; /* 指向对齐后的内存堆的最后一个内存块*/ static struct mem *ram_end; /* 指向已被释放的索引号最小的内存块内存堆最前面的已被释放的*/ static struct mem *LWIP_MEM_LFREE_VOLATILE lfree; ram_heap 数组就是lwIP 定义的内存堆总空间如何从这个总空间申请合适大小的内存 就是利用上述源码的三个指针ram 指针指向对齐后的内存堆总空间首地址ram_end 指针指向内存堆总空间尾地址接近总空间的尾地址而lfree 指针指向最低内存地址的空闲内存块。 注lwIP 内核就是根据lfree 指针指向空闲内存块来分配内存而ram_end 指针用来检测该总 内存堆空间是否有空闲的内存。 (5) 内存堆的初始化 结合以上的(1)~(4)的内容我们来看一下lwIP 动态内存堆是如何实现的如下源码所示 mem_init 函数 void mem_init(void) {struct mem *mem;/* 对内存堆的地址全局变量的名进行对齐指向ram_heap。*/ram (u8_t *)LWIP_MEM_ALIGN(LWIP_RAM_HEAP_POINTER);/* 建立第一个内存块内存块由内存块头空间组成。*/mem (struct mem *)(void *)ram;/* 下一个内存块不存在因此指向内存堆的结束*/mem-next MEM_SIZE_ALIGNED;/* 前一个内存块就是它自己因为这是第一个内存块*/mem-prev 0;/* 第一个内存块没有被使用*/mem-used 0;/* 初始化堆的末端指向MEM_SIZE_ALIGNED底部位置*/ram_end ptr_to_mem(MEM_SIZE_ALIGNED);/* 最后一个内存块被使用。因为其后面没有可用空间必须标记为已被使用*/ram_end-used 1;/* 下一个不存在因此指向内存堆的结束*/ram_end-next MEM_SIZE_ALIGNED;/* 前一个不存在因此指向内存堆的结束*/ram_end-prev MEM_SIZE_ALIGNED;/* 已释放的索引最小的内存块就是上面建立的第一个内存块。*/lfree (struct mem *)(void *)ram;/* 这里建立一个互斥信号量主要是用来进行内存的申请、释放的保护*/if (sys_mutex_new(mem_mutex) ! ERR_OK){} }上述源码就是对堆空间初始化一开始lfree 指针指向第一个内存块该内存块有两个部 分组成一个是控制块struct mem 大小标志管理的内存是否可用另一个是可用内存。 ram_end 指针指向尾内存块它用来标志这个堆空间是否有可用内存若lfree 指针指向 ram_end 指针则该堆空间没有可用内存分配由此可以看出lfree 指针从堆空间低地址不断查找和划分内存最终在ram_end 指针指向的地址结束分配。内存堆初始化示意图如下所示 注struct mem 结构体的next 和prev 变量并不是指针类型它们保存的是内存块的索引 例如定义一个a[10]数组next 和prev 保存的是0~9 的索引号lwIP 内核根据索引号获取a 数 组的索引地址a[0~9]。 2. mem_malloc 函数 void * mem_malloc(mem_size_t size_in) {mem_size_t ptr, ptr2, size;struct mem *mem, *mem2;/*******第一检测用户申请的内存块释放满足LWIP的规则*******//*******第二从内存堆中划分用户的内存块******//* 寻找足够大的空闲块从最低的空闲块开始.*/for (ptr mem_to_ptr(lfree); ptr MEM_SIZE_ALIGNED - size;ptr ((struct mem *)(void *)ram[ptr])-next){mem ptr_to_mem(ptr); /* 取它的地址*//* 空间大小必须排除内存块头大小*/if ((!mem-used) (mem-next - (ptr SIZEOF_STRUCT_MEM)) size){/* 这个地方需要判断剩余的内存块是否可以申请size内存块*/if (mem-next - (ptr SIZEOF_STRUCT_MEM) (size SIZEOF_STRUCT_MEM MIN_SIZE_ALIGNED)){/* 上面注释一大堆主要就是说剩余内存可能连一个内存块的头都放不下了这个时候就没法新建空内存块。其索引也就不能移动*//* 指向申请后的位置即建立下一个未使用的内存块的头部。即插入一个新空内存块*/ptr2 (mem_size_t)(ptr SIZEOF_STRUCT_MEM size);/*从Ptr2地址开始创建mem2的结构体*/mem2 ptr_to_mem(ptr2); /* 调用(struct mem *)(void *)ram[ptr]; */mem2-used 0;/* 这个根据下面的if(mem2-next ! MEM_SIZE_ALIGNED)判定*/mem2-next mem-next;mem2-prev ptr; /* 空闲内存块的前一个指向上面分配的内存块*//* 前一个内存块指向上面建立的空闲内存块*/mem-next ptr2;mem-used 1; /* 将当前分配的内存块标记为已使用*//* 如果mem2内存块的下一个内存块不是链表中最后一个内存块结束地址那就将它下一个的内存块的prve指向mem2 */if (mem2-next ! MEM_SIZE_ALIGNED){((struct mem *)(void *)ram[mem2-next])-prev ptr2;}}else{ /* 内存块太小了会产生的碎片*/mem-used 1;}/* 这里处理当分配出去的内存正好是lfree时因为该内存块已经被分配出去了必须修改lfree的指向下一个最其前面的已释放的内存块*/if (mem lfree){struct mem *cur lfree;/* 只要内存块已使用且没到结尾则继续往后找*/while (cur-used cur ! ram_end){cur ptr_to_mem(cur-next); /* 下一个内存块*/}/* 指向找到的第一个已释放的内存块。如果上面没有找到则lfree lfree不变*/lfree cur;}/* 这里返回内存块的空间的地址排除内存块的头*/return (u8_t *)mem SIZEOF_STRUCT_MEM MEM_SANITY_OFFSET;}}return NULL; } } 从上述源码可以看出lwIP 内存堆申请的内存是从低地址往高地址方向查找合适的内存 块每一个内存块由两个部分组成一个是struct mem大小的内存块它用来描述和管理 可用的内存块另一个是可用内存块用户可直接操作它。根据上图4.2.1 图解可以看出 lfree 指针指向的是未被使用的控制块若用户申请size 大小的内存则lwIP 内核会把lfree 指 针指向的控制块标志为已用内存并且往高地址偏移struct mem结构体对齐后的size 大 小偏移完成之后lfree 指针指向的地址附加一个struct mem 结构体下一个控制块。注下 一个控制块被标志为未使用即used0至此我们可以得到以下示意图。 3. mem_free 函数 void mem_free(void *rmem) {struct mem *mem;/* 第一步检查内存块的参数*//* 判断释放的内存块释放为空*/if (rmem NULL){return; /* 为空则返回*/}/* 除去指针就剩下内存块了通过mem_malloc的到的地址是不含struct mem 的*/ * /mem (struct mem *)(void *)((u8_t *)rmem - (SIZEOF_STRUCT_MEM MEM_SANITY_OFFSET));/* 第二步查找指定的内存块标记为未使用*/mem-used 0;/* 第三步需要移动全局的释放指针因为lfree始终指向内存堆中最小索引的那个已经释放的内存块*/if (mem lfree){/* 新释放的结构现在是最低的*/lfree mem;} }lwIP 内存堆释放内存是非常简单的它一共分为三个步骤第一、检测传入的地址是否 正确第二、对这个地址进行偏移偏移大小为struct mem这样可以得到释放内存的控制块 首地址并且设置该控制块为未使用标志第三、判断该控制块的地址是否小于lfree 指针指 向的地址若小于则证明mem 的内存块在lfree 指向的内存块之前即更接近堆空间首地址 系统会把lfree 指针指向这个释放的内存块控制块 可用内存以后申请内存时会在lfree 指针的内存块开始查找合适的内存。注申请内存时lwIP 内核会从lfree 指针指向的内存块开 始查找若该内存块不满足申请要求则lwIP 内核根据这个内存块的next 变量保存的数值作 为下一跳查询的地址。 若申请内存时lfree 指针指向的内存块不满足申请需求则该内存块的next 数值作为下一 跳查询的索引。注lfree 指针永远指向最低地址的内存空间。 动态内存池管理策略 在内存池初始化时候系统会将可用的内存块划分为N 个固定大小的内存这些内存块 通过单链表的方式连接起来在用户申请内存块时直接从单链表的头部取出一个内存块进行 分配释放内存块时也是挺简单的只要将内存块释放到链表的头部即可虽然这样的分配很 高效但是有很明显的缺点如浪费资源等。 lwIP 内存池的实现是受制于两个宏值MEMP_MEM_MALLOC 和MEM_USE_POOLS 的限 制在该动态内存池的源码文件中仍然到处可见这两个宏值。 1IP 内存池的应用场景 lwIP 存在很多固定的数据结构这些结构的特点就是在使用之前就已经知道了数据结构 的大小而且这些数据结构在使用的过程中不会发生大小改变的。比如在建立一个TCP 连接 的时候lwIP 需要使用一种叫做TCP 控制块的数据结构这种数据结构大小是固定的所以 为了满足这些数据类型分配的需要在内存初始化的时候就建立了一定数量的动态内存池 POOL。 2IP 内存池实现的文件 对于内存堆来说动态内存池分配还是挺麻烦的主要就是对于宏的巧妙运用现在笔者 就以文件的形式讲解动态内存池分配的原理。动态内存池分配在这四个文件memp.c、memp.h、 memp_std.h 和memp_prive.h 有所介绍下面笔者分别地讲解这四个文件的作用。 (1) memp_std.h 文件 该文件定义了lwIP 内核所需的内存池由于lwIP 内核的固定数据结构多种多样所以它 们使用宏定义声明是否使用该类型的内存池如TCP、UDP、DHCP、ICMP 等协议。这些宏 定义一般在lwippools.h 文件中声明启用。该文件的源码如下所示 #if LWIP_RAW LWIP_MEMPOOL(RAW_PCB, MEMP_NUM_RAW_PCB, sizeof(struct raw_pcb), RAW_PCB) #endif /* LWIP_RAW */ #if LWIP_UDP LWIP_MEMPOOL(UDP_PCB, MEMP_NUM_UDP_PCB, sizeof(struct udp_pcb), UDP_PCB) #endif /* LWIP_UDP */ #if LWIP_TCP LWIP_MEMPOOL(TCP_PCB, MEMP_NUM_TCP_PCB, sizeof(struct tcp_pcb), TCP_PCB) LWIP_MEMPOOL(TCP_PCB_LISTEN, MEMP_NUM_TCP_PCB_LISTEN,sizeof(struct tcp_pcb_listen), TCP_PCB_LISTEN) LWIP_MEMPOOL(TCP_SEG, MEMP_NUM_TCP_SEG, sizeof(struct tcp_seg), TCP_SEG) #endif /* LWIP_TCP */ /* …………………………………………………………………………………忽略以下源码……………………………………………………………………………………… */ 从上述源码可以看出两个重点内容第一点不同类型的内存池是由相应的宏定义声明启 用第二点LWIP_MEMPOOL 宏定义用来初始化各类型的内存池。 (2) memp_priv.h 文件 /* 管理内存块*/ struct memp {struct memp *next; }; /* 管理和描述各类型的内存池*/ struct memp_desc {/** 每个内存块的大小*/u16_t size;/** 内存块的数量*/u16_t num;/** 指向内存的基地址*/u8_t *base;/** 每个池的第一个空闲元素。元素形成一个链表*/struct memp **tab; }; 这个文件主要定义了两个结构体它们分别为memp 和memp_desc 结构体其中memp 结构体是把同一类型的内存池以链表的形式链接起来而memp_desc 结构体是用来管理和描 述各类型的内存池如数量、大小、内存池的起始地址和指向空闲内存池的指针。memp 和 memp_desc 结构体的关系如下图所示 从上图可以看出每一个描述符都是用来管理同一类型的内存池而这些内存池即内存块 是以链表的形式链接起来。 (3) memp.h 文件 在memp.h 文件中笔者重点讲解memp_t 枚举类型以及LWIP_MEMPOOL_DECLARE 宏 定义它们的作用如下所示 typedef enum { /* ##为C语言的连接符例如MEMP_##A,A NAME ,所以等于MEMP_NAME */ #define LWIP_MEMPOOL(name, num, size, desc) MEMP_##name, #include lwip/priv/memp_std.hMEMP_MAX } memp_t; #include lwip/priv/memp_priv.h /* 该文件需要使用上面的枚举*/ #include lwip/stats.h该文件最主要的是memp_t 枚举类型它主要获取各类内存池的数量这里用到宏的巧妙 运用根据memp_std.h 文件启用的内存池来计算各类内存池的数量MEMP_MAX。如何计算 请看下面内容 1LWIP_MEMPOOL 宏定义指向MEMP_##name##是C 语言的连接符 2根据#include lwip/priv/memp_std.h 文件启用了哪些类型内存池。 如果memp_std.h 文件只启用了LWIP_RAW 和LWIP_UDP 类型的内存池那么 MEMP_MAX 变量就等于2。这个枚举类型展开之后如下源码所示 typedef enum {MEMP_RAW_PCB,MEMP_UDP_PCB,MEMP_MAX } memp_t;根据枚举类型的特性MEMP_RAW_PCB 为0MEMP_UDP_PCB 为1由此类推。 注memp.h 文件最主要的任务是计算各类的内存池最后得出MEMP_MAX 数值。 #define LWIP_MEMPOOL_DECLARE(name, num, size, desc) \ LWIP_DECLARE_MEMORY_ALIGNED(memp_memory_ ## name ## _base, ((num) * (MEMP_SIZE MEMP_ALIGN_SIZE(size))));LWIP_MEMPOOL_DECLARE_STATS_INSTANCE(memp_stats_##name)static struct memp *memp_tab_##name;const struct memp_desc memp_##name {DECLARE_LWIP_MEMPOOL_DESC(desc)LWIP_MEMPOOL_DECLARE_STATS_REFERENCE(memp_stats_##name)LWIP_MEM_ALIGN_SIZE(size),(num),memp_memory_##name##_base,memp_tab_##name}; \ }; 此宏定义非常重要各类型的内存池都使用这个宏定义声明例如内存池的内存由来各 类型内存池的数量、大小、内存由来的地址以及指向空闲的指针。这个宏定义展开后如下源码 所示 #define LWIP_MEMPOOL_DECLARE(name,num,size,desc) \ u8_t memp_memory_ ## name ## _base[((((((num) * (MEMP_SIZE (((size) MEM_ALIGNMENT - 1U) ~(MEM_ALIGNMENT-1U))))) MEM_ALIGNMENT - 1U)))];\ static struct memp *memp_tab_ ## name;\ const struct memp_desc memp_ ## name { \ LWIP_MEM_ALIGN_SIZE(size), \ (num), \ memp_memory_ ## name ## _base, \ memp_tab_ ## name \ };展开之后可以看出各类型的内存池的内存由来和lwIP 内存堆一样都是由数组分配的。 这个宏定义的使用笔者会在memp.c 文件中讲解。 (4) memp.c 文件 在讲解函数之前我们必须知道LWIP_MEMPOOL 和const memp_pools[MEMP_MAX]这 两部分的内容其中LWIP_MEMPOOL 指向LWIP_MEMPOOL_DECLARE 宏定义该宏定义 笔者已经在memp.h 文件展开过稍后重点讲解而const memp_pools[MEMP_MAX]数组是用 来管理各类型的内存池描述符。下面笔者分别地讲解这两部分的内容如下所示 #define LWIP_MEMPOOL(name,num,size,desc)LWIP_MEMPOOL_DECLARE(name,num,size,desc) #include lwip/priv/memp_std.h这里也是一样对宏的巧妙运用例如memp_std.h 只启用LWIP_RAW 和LWIP_UDP 类 型的内存池展开之后如下所示 u8_t memp_memory_RAW_PCB_base[((((((num) * (MEMP_SIZE (((size) MEM_ALIGNMENT - 1U) ~(MEM_ALIGNMENT - 1U))))) MEM_ALIGNMENT - 1U)))]; static struct memp *memp_tab_RAW_PCB; const struct memp_desc memp_RAW_PCB {LWIP_MEM_ALIGN_SIZE(size),(num),memp_memory_TCPIP_MSG_API_base,memp_tab_TCPIP_MSG_API}; u8_t memp_memory_UDP_PCB_base[((((((num) * (MEMP_SIZE (((size) MEM_ALIGNMENT - 1U) ~(MEM_ALIGNMENT - 1U))))) MEM_ALIGNMENT - 1U)))]; static struct memp *memp_tab_UDP_PCB; const struct memp_desc memp_UDP_PCB {LWIP_MEM_ALIGN_SIZE(size),(num),memp_memory_UDP_PCB_base,memp_tab_UDP_PCB};\ };LWIP_MEMPOOL_DECLARE 宏定义展开流程笔者已经上面讲解过这里无需重复讲解。 总的来说这两段代码声明了各类内存池描述和管理信息例如memp_desc memp_ RAW_PCB 结构体它描述了该类型的内存池的数量、大小、分配内存地址以及指向空闲内存 池的指针。 const struct memp_desc *const memp_pools[MEMP_MAX] { #define LWIP_MEMPOOL(name, num, size, desc) memp_##name, #include lwip/priv/memp_std.h };这一个数组的大小就是由MEMP_MAX 变量声明这个变量无需讲解请看上面的内容。 若memp_std.h 只启用LWIP_RAW 和LWIP_UDP 类型的内存池则这个数组展开之后如下所示 const struct memp_desc* const memp_pools[MEMP_MAX] {memp_memp_RAW_PCB,memp_memp_UDP_PCB, };数组的第一个元素取memp_memp_RAW_PCB 地址它就是我们前面展开之后的 memp_RAW_PCB 变量。 memp_init 函数和memp_init_pool 函数 该函数是内存池的初始化该函数如下所示 void memp_init(void) {u16_t i;/* 遍历需要多少个内存池*/for (i 0; i LWIP_ARRAYSIZE(memp_pools); i){memp_init_pool(memp_pools[i]);} } void memp_init_pool(const struct memp_desc *desc) {int i;struct memp *memp;*desc-tab NULL;/* 内存对齐*/memp (struct memp *)LWIP_MEM_ALIGN(desc-base);/* 将内存块链接成链表形式*/for (i 0; i desc-num; i){memp-next *desc-tab;*desc-tab memp;/* 地址偏移*/memp (struct memp *)(void *)((u8_t *)memp MEMP_SIZE desc-size);} }从上述源码可以看出每一个类型的描述符都是用来管理和描述该类型的内存池这些同 一类型的内存池里面包含了指向下一个节点的指针根据第二个for 循环语句让这些同一类型 的内存池以链表的形式链接起来最后不断的循环我们可以得到以下示意图 从上图可知memp_pool 数组包含了多个类型的内存池描述符这些描述符管理同一类型 的内存池这些内存池以链表的形式链接起来最后形成一个单向链表。注同一类型的内存 池都是在同一个数组分配得来而base 指针指向该数组的首地址tab 指针指向第一个空闲的 内存池若用户向申请一个内存池则从tab 指针指向的内存池分配分配完成之后tab 指针 偏移至下一个空闲内存池的地址。 memp_malloc 函数和memp_malloc_pool 函数 前面讲解到内存池具有多种类型的所以用户申请内存池时必须知道申请内存池的类 型是哪个lwIP 内存池申请函数为memp_malloc该函数如下所示 void * memp_malloc(memp_t type) {void *memp;memp do_memp_malloc_pool(memp_pools[type]);return memp; } static void * do_memp_malloc_pool(const struct memp_desc *desc) {struct memp *memp;memp *desc-tab;if (memp ! NULL){*desc-tab memp-next;return ((u8_t *)memp MEMP_SIZE);}else{}return NULL; } memp_malloc 函数需要传入申请内存池的类型如UDP_PCB…接着根据传入的类型来 查找对应的内存池描述符查找完成之后根据该内存池描述符的tab 指针指向内存池分配给用 户并且把tab 指针偏移至下一个空闲内存池。分配流程如下图所示 memp_free 函数与memp_free_pool 函数 内存池释放函数非常简单它需要传入两个形参第一个是释放内存池的类型第二个是 释放内存池的地址。lwIP 内核根据这两个形参就可以知道该类型的内存池描述符位置和该类 型内存池描述符的哪个内存池需要释放。内存池释放函数如下所示 void memp_free(memp_t type, void *mem) {if (mem NULL) /* 判断内存块的起始地址释放为空*/{return;}do_memp_free_pool(memp_pools[type], mem); } static void do_memp_free_pool(const struct memp_desc *desc, void *mem) {struct memp *memp;/* 据内存块的地址偏移得到内存块的起始地址*/memp (struct memp *)(void *)((u8_t *)mem - MEMP_SIZE);/* 内存块的下一个就是链表中的第一个空闲内存块*/memp-next *desc-tab;/* *desc-tab指向memp内存块中*/*desc-tab memp; }释放函数非常简单只需对内存池描述符的tab 指针偏移至释放的内存池。释放流程如下 图所示 使用C 库管理内存策略 lwIP 内核是可以支持C 标准库管理策略它与lwIP 内存堆管理策略二者只能选其一。打 开mem.c 文件找到MEM_LIBC_MALLOC 配置项如下源码所示 /* in case C library malloc() needs extra protection,* allow these defines to be overridden.*/ #ifndef mem_clib_free #define mem_clib_free free #endif #ifndef mem_clib_malloc #define mem_clib_malloc malloc #endif #ifndef mem_clib_calloc #define mem_clib_calloc calloc #endif #if LWIP_STATS MEM_STATS #define MEM_LIBC_STATSHELPER_SIZE LWIP_MEM_ALIGN_SIZE(sizeof(mem_size_t)) #else #define MEM_LIBC_STATSHELPER_SIZE 0 #endif上述的free、malloc 以及calloc 就是C 库中的内存管理函数。注C 标准库内存管理不能 与相邻的空闲内存块合并且容易造成内存碎片。 lwIP 网络接口管理(管理不同网卡) lwIP 支持多网口设计它是使用netif 来描述每种网络接口的特性如IP 地址、接口状态等。为了实现对所有网络接口的有效管理协议栈内部使用了一个名为netif 的网络接口结构来描述各种网络设备如果项目中使用多个网卡那么lwIP 是如何管理这些网卡的呢链表netif_list就是管理多个netif 网络接口的当上层应用有数据要发送的时候lwIP 会从netif_list 链表中选择一个合适的网卡来将数据发送出去。 网络接口结构netif 总所周知网卡的种类多种多样的对于lwIP 来说它是怎么样兼容众多网卡的呢 lwIP 有一个数据结构—netif 来描述一个网卡因为网卡是与硬件相关的不同的硬件处理的 方式也是不同的所以lwIP 提供了统一接口函数来管理这些网卡。由于网卡的种类繁多所 以各个网卡的底层函数需要用户来完成例如网卡的初始化、网卡的接收、发数据等函数同样 lwIP 底层得到网络数据时需要层层递交才会传入内核处理相反lwIP 发送数据也是调用网 卡发送函数。对于没有接触lwIP 的学员来说我们该怎么样写底层驱动呢lwIP 还是做的挺 好的它已经提供了一个ethernetif.c 文件该文件是底层接口的驱动模版用户根据自己的网 络设备参照修改即可。 下面笔者来讲解netif 的数据结构该数据结构是在netif.h 文件中定义的该结构如下所 示 struct netif {/* 指向下一个netif结构的指针*/struct netif *next;/* IP地址相关配置*/ip_addr_t ip_addr; /* 网络接口的IP 地址*/ip_addr_t netmask; /* 子网掩码*/ip_addr_t gw; /* 网关地址*//* 该函数向IP层输入数据包*/netif_input_fn input;/* 该函数发送IP包--检测目标IP地址的MAC地址等操作*/netif_output_fn output;/* 该函数实现底层数据包发送*/netif_linkoutput_fn linkoutput;/* 该字段用户可以自由设置例如用于指向一些底层设备相关的信息*/void *state;void *client_data[LWIP_NETIF_CLIENT_DATA_INDEX_MAX LWIP_NUM_NETIF_CLIENT_DATA];/* 该接口允许的最大数据包长度大于1500则分片处理*/u16_t mtu;/* 该接口物理地址长度*/u8_t hwaddr_len;/* 该接口的物理地址*/u8_t hwaddr[NETIF_MAX_HWADDR_LEN];/* 该接口的状态、属性字段*/u8_t flags;/* 该接口的名字*/char name[2];/* 接口的编号*/u8_t num;/* 需要发送的路由器请求消息的数量*/u8_t rs_count; };该结构体包含了多个字段这些字段的作用如下 (1) next: 该字段指向下一个neitif 类型的结构体因为lwIP 可以支持多个网络接口当设 备有多个网络接口的话lwIP 就会把所有的netif 结构体组成链表来管理这些网络接口。 (2) ipaddrnetmask 和gw分别为网络接口的IP 地址、子网掩码和默认网关。 (3) input此字段为一个函数这个函数将网卡接收到的数据交给IP 层。 (4) output此字段为一个函数当IP 层向接口发送一个数据包时调用此函数。这个函数 通常首先解析硬件地址然后发送数据包。此字段我们一般使用etharp.c 中的 etharp_output()函数。 (5) linkoutput此字段为一个函数该函数被ARP 模块调用完成网络数据的发送。上 面说的etharp_output 函数将IP 数据包封装成以太网数据帧以后就会调用linkoutput 函 数将数据发送出去。 (6) state用来定义一些关于接口的信息用户可以自行设置。 (7) mtu网络接口所能传输的最大数据长度一般设置为1500。 (8) hwaddr_len网卡MAC 地址长度6 个字节。 (9) hwaddrMAC 地址。 (10) flags网络的接口状态属性信息字段。 (11) name网卡的名字。 (12) num编号从0 开始此字段为协议栈为每个网络接口设置的一个编号。 (13) rs_count发送的路由器请求消息的数量。 这些字段就是用来描述各个网卡的差异每一个网卡都使用一个netif 结构体来抽象多 个网卡就有多个netif这些netif 以链表的形式链接起来形参一个单向的链表。 这些netif 链表的首个节点由netif_list 指针指向lwIP 内核就是使用netif_list 指针对netif 链表进行遍历查询。管理和描述netif 链表由三个全局变量这些变量如下所示 struct netif *netif_list; /* 网络接口链表指针*/ struct netif *netif_default; /* 哪个网络接口(多网口时候) */ static u8_t netif_num; /* 为网口分配唯一标识*/netif_default 指针指向netif 链表的默认网卡如网络层下发一个数据包时系统优先选择 netif_default 指针指向的网卡发送数据如该网卡没有响应则选择其他的网卡发送。 netif_num 描述网卡的数量。 下面笔者重点讲解netif.c 重要的几个函数这些函数如下所示 (1) netif_add 函数 该函数是把新创建的netif 插入到netiflist 队列当中以表示添加一个网络接口该函数如下所示 struct netif * netif_add(struct netif *netif,const ip4_addr_t *ipaddr, const ip4_addr_t *netmask,const ip4_addr_t *gw,void *state, netif_init_fn init, netif_input_fn input) {/* 清空主机IP 地址、子网掩码、网关等信息。*/ip_addr_set_zero_ip4(netif-ip_addr);ip_addr_set_zero_ip4(netif-netmask);ip_addr_set_zero_ip4(netif-gw);netif-output netif_null_output_ip4; // 调用实际的发送函数 netif_null_output_ip4/* 传输的最大数据长度*/netif-mtu 1500;/* 网络的接口状态*/netif-flags 0;memset(netif-client_data, 0, sizeof(netif-client_data));/* 传递进来的参数填写网卡state、input等字段的相关信息*/netif-state state;/* 并为当前网卡分配唯一标识num */netif-num netif_num;/* 网卡输入*/netif-input input; //调用实际的接收函数 input/* 调用网卡设置函数netif_set_addr()设置网卡IP 地址、子网掩码、网关*/netif_set_addr(netif, ipaddr, netmask, gw);/* 为netif调用用户指定的初始化函数*/if (init(netif) ! ERR_OK){return NULL;}/* 将这个netif添加到列表中*/netif-next netif_list;netif_list netif;mib2_netif_added(netif);netif_invoke_ext_callback(netif, LWIP_NSC_NETIF_ADDED, NULL);return netif; }从上述源码可以看出每一个netif 结构体就是对一个网卡进行抽象例如该网卡的收发 函数、状态等信息。根据上述函数的运行流程可得到以下示意图 ①只有一个网络接口 ②两个网络接口 注新插入的netif 结构体是在netiflist 队列的首部插入。 (2) netif_set_default 函数 该函数就是设置某一个netif 结构体为默认的网卡lwIP 内核优先对这个网卡操作该函 数如下所示 void netif_set_default(struct netif *netif) {if (netif NULL){/* 删除默认路由*/mib2_remove_route_ip4(1, netif);}else{/* 添加默认路由*/mib2_add_route_ip4(1, netif);}netif_default netif; /* 选择那个网络接口*/ } /*********************怎么使用函数netif_set_default()*********************/ /* 通过该函数将网络接口添加到链表中*/ netif_add(xnetif, ipaddr, netmask, gw, NULL, ethernetif_init, tcpip_input); /* 注册默认的网络接口*/ netif_set_default(xnetif); 方法很简单就是让netif_default 指针指向默认的网卡。 lwIP 网络数据包管理 TCP/IP 协议本质上就是对数据包的处理过程lwIP 作者为了提高对数据包的处理工作效 率它提供一种高效的数据包管理机制使得各层之间对数据包灵活操作同时避免在各层之 间的复制数据的巨大开销和减少各层间的传递时间。在linux 的BSD 协议中它描述数据包的 结构体叫做mbuf而lwIP 与它类似的结构叫做pbufpbuf 数据包的种类和大小也可以说是多 种多样的从网卡读取出来的数据包可以是一千个字节也可以是几个字节的IP 数据报这些 数据包可能存在于RAM 和ROM 中这个根据用户来决定的所以lwIP 为了处理的数据高效 它需要把这些数据进行统一的管理。 TCP/IP 协议各层间的操作 我们知道传统的TCP/IP 协议各层之间是独立存在的每一层只处理该层的数据它们绝 对不允许越界读写数据如果lwIP 按照这种严格的分层模式来实现TCP/IP 协议会使数据包 在各层间的递交变得非常慢它涉及到一系列的内存拷贝的问题所以系统总体性能也会受到 影响。因此lwIP 内部并没有采用完整的分层结构它会假设各层间的部分数据结构和实现 原理在其他层是可见的这样在数据包递交过程中各层协议可以直接对数据包中属于其他层 次协议的字段进行操作。 从上述可以看出lwIP 的优点有以下几个 ①不需要数据层层拷贝。 ②用户程序可以直接访问内部各层数据包。 ③各个层次之间存在交叉存取数据的现象既节省系统的空间也节省处理的时间而且 更加灵活。 ④lwIP 的内存共享机制使得应用程序能直接对协议栈内核的内存区域直接操作减少 时间和空间的损耗。 lwIP 的线程简介 在操作系统中任务的创建与任务管理是常见的东西如果把协议栈的各层变成独立的任 务或者线程那么会导致各层之间是严格分层的在这种模式下能够使编程简便、代码组织 灵活但是缺点也是很明显的例如数据递交时需要进行拷贝和切换任务任务或者线程频繁 切换可能对用户程序不能够准时的处理一个数据包在各个层次间的递交至少需要进行3 次切 换任务如应用层发送数据时需要切换到传输层任务处理当传输层处理完毕之后会把数据 报递交给网络层由此类推导致任务频繁切换使得协议栈的效率低下。 还有一种方法就是协议栈与操作系统结合相当于把协议栈成为操作系统的一部分这样 用户任务与协议栈之间通过操作系统的API 函数实现虽然提高了效率各层也可以交叉存 取但是协议栈与操作系统融合会导致很严重的后果总所周知操作系统最大的优势是实时 性高能准确的运行相关的线程如果协议栈成为了操作系统的一部分那么协议栈处理的数 据包过慢的话会导致操作系统的实时性变低。 lwIP 采用了另一种方式让协议栈与操作系统相互隔离这样不会影响操作系统的实时性协议栈只作为操作系统的一个独立的任务这样我们可以得出两个方法第一种方法就是 让用户程序驻留在协议栈任务里协议栈通过回调函数实验用户与协议栈之间的数据交互这 个也是lwIP 所说的RAW API 编程。第二种方法就是用户程序可以作为操作系统的独立任务 用户任务与协议栈任务之间的通信通多IPC 通信机制交互这种在lwIP 叫做NETCONN API 和Socket API 编程。 网络数据包 pbuf 结构 lwIP 使用pbuf 对数据进行发送与接收灵活的pbuf 结构体使得数据在不同层之间传输时 可以减少内存的开销以及减少内存复制所占用的时间一切都是为了节约内存提高数据在不 同层之间传递的速度。lwIP 源码中的pbuf.c 和pbuf.h 这两个文件就是关于pbuf 的pbuf 结构 如下源码所示 struct pbuf {/* pbuf链表中指向下一个pbuf结构*/struct pbuf *next;/* 数据指针指向该pbuf所记录的数据区域通过指针偏移*/void *payload;/* 当前pbuf及后续所有pbuf中所包含的数据总长度*/u16_t tot_len;/* 当前pbuf中数据的长度*/u16_t len;/* 当前pbuf的类型*/u8_t type;/* 状态位未用到*/u8_t flags;/* 指向该pbuf的指针数即该pbuf被引用的次数*/LWIP_PBUF_REF_T ref;/* 对于传入的数据包它包含输入netif的索引*/u8_t if_idx; };pbuf 结构体具有多个字段这些字段的作用如下所示 从表可以看出pbuf 具有四个类型它们的数据存储在不同的区域下面笔者重点讲解 着四个类型的pbuf。 (1) PBUF_RAM 类型 PBUF_RAM 是lwIP 用的最多的一种类型pbuf 空间大小是通过内存堆来分配的一般协议栈中要发送的数据都是采用这种形式这个类型也是常用的类型之一申请PBUF_RAM 类型的pbuf 时协议栈会在内存堆中分配相应空间这里的大小包括如前面所述的pbuf 结构和相应数据缓冲区的大小并且它们是在一片连续的存储空间。分配完成后的结构如下图所示 注payload 指向并不一定是数据区域的首地址可以设定一定的offset 偏移这个offset偏移量常用来存储TCP 报文首部、IP 首部等。当然layer 的大小也可以是0具体是多少就与数据包的申请方式有关。 (2) PBUF_POOL 类型 PBUF_POOL 类型和PBUF_RAM 类型的pbuf 有很大的相似之处不同之处时它的空间通过内存池分配得到的这种类型的pbuf 可以在极短的时间内得到分配。 在网卡接收数据包的时候我们就使用这种方式包装数据或者存储接收到的数据。其中在系统初始化内存池的时候还会初始化两类与数据报pbuf 密切相关的POOL如下源码所示 LWIP_PBUF_MEMPOOL(PBUF, MEMP_NUM_PBUF, 0, PBUF_REF/ROM) LWIP_PBUF_MEMPOOL(PBUF_POOL,PBUF_POOL_SIZE, PBUF_POOL_BUFSIZE, PBUF_POOL)内存池是一个固定大小的内存块若用户数据大于固定大小的内存池则lwIP 内核会以多个固定大小的内存池来存储这些数据存储完成之后系统把多个pbuf 以链表的形式链接起来构建了一个单向链表如下图所示 (3) PBUF_ROM和PBUF_REF 类型 剩余的两个PBUF_ROM 和PBUF_REF 比较类似它们都是在内存池中分配一个相应的pbuf 结构但不申请数据区的空间它们两者的区别在于PBUF_ROM 指向ROM 空间内的数据后者指向RAM 空间内的某段数据。在发送某些静态数据时可以采用这两种类型的pbuf这可以大大节省协议栈的内存空间结构如下图所示 另外对于一个数据包来讲它可能使用上述任意的pbuf 类型来描述还可以一大串不同类型的pbuf 连在一起共同保存一个数据包的数据如下图所示 lwIP 网络数据包pbuf 提供了5 个函数这些函数如下所示 pbuf_alloc 函数 该函数根据类型、大小和偏移来申请pbuf 空间若该函数返回NULL则申请失败。 static void pbuf_init_alloced_pbuf(struct pbuf *p, void *payload, u16_t tot_len, u16_t len, pbuf_type type, u8_t flags) {p-next NULL; /* 指向NULL */p-payload payload; /* 指向数据区域*/p-tot_len tot_len; /* 总长度*/p-len len; /* 该pbuf长度*/p-type_internal (u8_t)type; /* 申请的pbuf类型*/p-flags flags; /* 状态位*/p-ref 1; /* 指向该pbuf的指针数即该pbuf被引用的次数*/p-if_idx NETIF_NO_INDEX; /* 对于传入的数据包它包含输入netif的索引*/ } struct pbuf * pbuf_alloc(pbuf_layer layer, u16_t length, pbuf_type type) {struct pbuf *p;u16_t offset (u16_t)layer; /* 申请那个层的首部*//* 判断以太网首部*/switch (type){case PBUF_REF: /* 失败*/case PBUF_ROM:p pbuf_alloc_reference(NULL, length, type);break;case PBUF_POOL:{struct pbuf *q, *last;u16_t rem_len; /* 总大小*/p NULL;last NULL;rem_len length; /* rem_len赋值为总长度*/do{u16_t qlen; /* 减去首部的长度*//* 申请内存池*/q (struct pbuf *)memp_malloc(MEMP_PBUF_POOL);if (q NULL) /* 申请内存池失败*/{PBUF_POOL_IS_EMPTY();if (p){pbuf_free(p);}return NULL;}/* 总长度减去offset首部大小并赋值给qlen去除首部的长度LWIP_MIN(x , y) (((x) (y)) ? (x) : (y)) */qlen LWIP_MIN(rem_len, (u16_t)(PBUF_POOL_BUFSIZE_ALIGNED – LWIP_MEM_ALIGN_SIZE(offset)));/* 分配后初始化struct pbuf成员*/pbuf_init_alloced_pbuf(q, LWIP_MEM_ALIGN((void *)((u8_t *)q SIZEOF_STRUCT_PBUF offset)),rem_len, qlen, type, 0);if (p NULL) /* 第一次分配p必定指向NULL */{/* pbuf链分配头*/p q;}else/* 让前面的pbuf指向这个pbuf */last-next q;}last q;/* 判断是否还有剩余长度*/rem_len (u16_t)(rem_len - qlen);offset 0;}while (rem_len 0); /* 如果有剩余还需要执行一次do语句*/break;} case PBUF_RAM: {u16_t payload_len (u16_t)(LWIP_MEM_ALIGN_SIZE(offset) LWIP_MEM_ALIGN_SIZE(length));mem_size_t alloc_len (mem_size_t)(LWIP_MEM_ALIGN_SIZE(SIZEOF_STRUCT_PBUF) payload_len);if ((payload_len LWIP_MEM_ALIGN_SIZE(length)) ||(alloc_len LWIP_MEM_ALIGN_SIZE(length))){return NULL;}/* 如果要在RAM中分配pbuf请为它分配内存。*/p (struct pbuf *)mem_malloc(alloc_len);if (p NULL){return NULL;}pbuf_init_alloced_pbuf(p, LWIP_MEM_ALIGN((void *)((u8_t *)p SIZEOF_STRUCT_PBUF offset)),length, length, type, 0);break; } default:return NULL; } return p; }此函数首先判断申请pbuf 的类型根据type 的值来运行相应的代码段layer 变量是为了让pbuf 中的payload 指针偏移lwIP 网络数据包pbuf 就是根据这个指针偏移来添加各层的首部。 pbuf_free 函数 此函数是对各类型的数据包pbuf 进行释放该函数实现原理如下所示 u8_t pbuf_free(struct pbuf *p) {u8_t alloc_src;struct pbuf *q;u8_t count;/* 如果数据包为空则返回0 */if (p NULL){return 0;}PERF_START;count 0;/* 判断数据包不为空*/while (p ! NULL){LWIP_PBUF_REF_T ref;SYS_ARCH_DECL_PROTECT(old_level);SYS_ARCH_PROTECT(old_level);/* 减少引用计数(指向pbuf的指针数) */ref --(p-ref);SYS_ARCH_UNPROTECT(old_level);if (ref 0){/* 为了下一次迭代请记住链中的下一个pbuf */q p-next;alloc_src pbuf_get_allocsrc(p); #if LWIP_SUPPORT_CUSTOM_PBUF/* is this a custom pbuf? */if ((p-flags PBUF_FLAG_IS_CUSTOM) ! 0){struct pbuf_custom *pc (struct pbuf_custom *)p;pc-custom_free_function(p);}else #endif /* LWIP_SUPPORT_CUSTOM_PBUF */{/* 判断释放的内存池的类型*/if (alloc_src PBUF_TYPE_ALLOC_SRC_MASK_STD_MEMP_PBUF_POOL){memp_free(MEMP_PBUF_POOL, p);/* is this a ROM or RAM referencing pbuf? */}else if (alloc_src PBUF_TYPE_ALLOC_SRC_MASK_STD_MEMP_PBUF){memp_free(MEMP_PBUF, p);/* type PBUF_RAM */}else if (alloc_src PBUF_TYPE_ALLOC_SRC_MASK_STD_HEAP){mem_free(p);}else{}}count;/* 继续到下一个pbuf */p q;}else{p NULL;}}return count; }此函数可以分为两个部分讲解第一、对pbuf 的ref 参数减1 操作并调用memp_free/mem_free 释放内存池或内存堆第二、如果数据包具有两个或者两个以上的pbuf 链表也 是和第一点一样的操作。 pbuf_realloc 函数 把相应的pbuf 链表尾部释放一定的空间并将在数据包pbuf 的数据长度减少到某个长度值注意该函数只是修改pbuf 中的长度字段值并不释放对应的内存池空间。 pbuf_header 函数 用于调整pbuf 的payload 指针向前或向后移动一定字节数可以调用pbuf_header 函数使payload 指针指向数据区前的首部字段这就为各层对数据包首部的操作提供了方便。当然进行这个操作的时候len 和tot_len 字段值也会随之改动。 pbuf_take 函数 用于向 pbuf 的数据区域拷贝数据。pbuf_copy 函数用于将一个任何类型的pbuf 中的数据拷贝到一个PBUF_RAM 类型的pbuf 中。pbuf_chain 函数用于连接两个pbuf链表为一个pbuf 链表。pbuf_ref 函数用于将pbuf 中的值加1。 网络接口接收数据 STM32 基本上使用ETH 接口来接收数据后产生一个ETH 中断在中断中释放一个信号 量s_xSemaphore通知网络接口任务ethernetif_input处理接收的数据这个任务对数据 封装成消息并传递给tcpip_mbox 邮箱以邮箱发送消息。lwIP 内核有一个协议栈线程它的 作用就是接收tcpip_mbox 邮箱的消息并且对接收的消息进行解析处理在处理之前先判断 消息的类型lwIP 内核根据消息的类型处理不同的代码段如下图所示 从上图可以看出ethernetif_input 是一个接收线程的任务函数它用来获取ETH 中断释放 的信号量若接收到信号量则调用low_level_input 函数获取描述符管理缓冲区的数据并且 把这些数据调用tcp_input 函数构建消息以tcpip_mbox 邮箱的方式发送消息。lwIP 内核在初 始化时创建了TCP/IP 线程它的作用是接收tcpip_mbox 邮箱的消息并且对接收的消息进 行解析处理在处理之前先判断消息的类型lwIP 内核根据消息的类型处理不同的代码段。 lwIP 超时处理 在lwIP 中很多时候会使用到超时处理超时处理的实现是TCP/IP 协议栈中一个重要部分。 它为每个与外界网络连接的任务都设定了timeout 属性即等待超时时间。 lwIP 中为什么需要做超时处理呢这可从其实现的TCP/IP 协议栈功能可以知道TCP 的建立连接超时、重传超时机制IP 分片数据报的重装等待超时ARP 缓存表项的时间管理、ping 接收数据包超时处理等等都需要使用超时操作来处理。超时处理的相关代码在timeouts.c/h 中实现下面笔者分别地讲解这两个文件的内容。 (1) timeouts.h 文件 该文件主要定义了两个结构体它们分别为lwip_cyclic_timer 和sys_timeo第一个结构体定义了超时等待时间和超时处理函数另外一个是管理这些超时的定时器着两个结构体的原型如下所示 lwip_cyclic_timer 结构体 struct lwip_cyclic_timer {u32_t interval_ms; /* 超时间隔*/lwip_cyclic_timer_handler handler; /* 超时处理函数*/ };const struct lwip_cyclic_timer lwip_cyclic_timers[] {{TCP_TMR_INTERVAL, HANDLER(tcp_tmr)},{IP_TMR_INTERVAL, HANDLER(ip_reass_tmr)},{ARP_TMR_INTERVAL, HANDLER(etharp_tmr)},{DHCP_COARSE_TIMER_MSECS, HANDLER(dhcp_coarse_tmr)},{DHCP_FINE_TIMER_MSECS, HANDLER(dhcp_fine_tmr)},{AUTOIP_TMR_INTERVAL, HANDLER(autoip_tmr)},{IGMP_TMR_INTERVAL, HANDLER(igmp_tmr)},{DNS_TMR_INTERVAL, HANDLER(dns_tmr)}, };可以看到interval_ms 变量就是超时等待时间而handler 就是超时处理函数即超时事件若超时了则触发一个超时事件。lwip_cyclic_timers 数组就是定义了lwIP 内核所需的超时定时器即超时事件。 sys_timeo 结构体 typedef void (*sys_timeout_handler)(void *arg); struct sys_timeo {struct sys_timeo *next; /* 下一个超时事件的指针*/u32_t time; /* 当前超时事件的等待时间*/sys_timeout_handler h; /* 指向超时的回调函数*/void *arg; /* 超时的回调函数参数*/ };这个结构体是用来管理这些超时事件它的next 指针指向下一个超时事件最后这些超时事件形成了单向链表。这些超时事件都调用同一的超时回调函数这个函数由h 函数指针指向最后根据arg 回调函数形参来调用哪个超时事件处理。注time 变量等于系统节拍加上超时等待时间例如系统当前节拍是1s超时定时器的等待时间为5s所以系统在节拍等于6s时才执行超时事件。 (2) timeouts.c 文件 注册超时事件 lwip_cyclic_timers 保存了lwIP 所需的超时事件这些超时事件由sys_timeouts_init 函数插入到超时链表当中该函数如下所示 void sys_timeouts_init(void) {size_t i;for (i (LWIP_TCP ? 1 : 0); i LWIP_ARRAYSIZE(lwip_cyclic_timers); i){sys_timeout(lwip_cyclic_timers[i].interval_ms, cyclic_timer,LWIP_CONST_CAST(void *, lwip_cyclic_timers[i]));} }此函数很简单获取lwip_cyclic_timers 元素地址和等待超时时间之后调用sys_timeout 函数把超时事件插入到超时链表当中。sys_timeout 函数如下所示 #define LWIP_MAX_TIMEOUT 0x7fffffff /* 当前插入超时事件时间与next_timeout指向超时事件时间对比是否大于0x7fffffff 如果t – compare_to为负值的话由于类型为u32_t所以导致该值比0x7fffffff 大 如果比LWIP_MAX_TIMEOUT 大则为1否则为0*/ #define TIME_LESS_THAN(t, compare_to) ((((u32_t)((t) - (compare_to))) \LWIP_MAX_TIMEOUT) \? 1 \: 0) void sys_timeout(u32_t msecs, sys_timeout_handler handler, void *arg) {u32_t next_timeout_time;/* 由TIME_LESS_THAN宏处理的溢出*/next_timeout_time (u32_t)(sys_now() msecs);sys_timeout_abs(next_timeout_time, handler, arg); }从这里可以看出next_timeout_time 变量等于系统当前节拍加上某个超时事件的等待时间其实next_timeout_time 变量最终赋给sys_timeo 结构体下的time 成员变量。这个超时事件由这个sys_timeout_abs 函数插入到超时链表当中该函数如下所示 static void sys_timeout_abs(u32_t abs_time, sys_timeout_handler handler, void *arg) {struct sys_timeo *timeout, *t;/* 申请节点内存*/timeout (struct sys_timeo *)memp_malloc(MEMP_SYS_TIMEOUT);if (timeout NULL){ /* 申请内存失败直接返回*/return;}/* 节点各变量赋值*/timeout-next NULL;timeout-h handler;timeout-arg arg;/* abs_time (u32_t)(sys_now() msecs) */timeout-time abs_time;/* 如果创建的是第一个定时器则不用特殊处理next_timeout是一个全局指针指向定时器链表中第一个定时器*/if (next_timeout NULL){next_timeout timeout;return;}/* 如果新添加的定时器小于当前链首定时器的时长则进入该代码段*/if (TIME_LESS_THAN(timeout-time, next_timeout-time)){timeout-next next_timeout;next_timeout timeout;}else{for (t next_timeout; t ! NULL; t t-next){if ((t-next NULL) || TIME_LESS_THAN(timeout-time, t-next-time)){timeout-next t-next;t-next timeout;break;}}} }首先此函数为超时事件申请内存以内存池的方式申请接着对超时事件各个成员变量赋值可以看到h 函数指针指向超时回调函数arg 指针指向lwip_cyclic_timers 数组的某个元素地址超时回调函数就是根据arg 形参来运行某个超时事件time 变量等于了next_timeout_time 变量即当前系统节拍加上超时等待时间最后插入到超时链表当中。下面笔者使用几个示意图来讲解这个函数如下所示 从上图可以知道该超时事件的time 等于21 即当前系统节拍加上超时事件等待函数它的next 指针指向为NULL因为一开始这个超时链表没有超时事件所以next_timeout 指向新插入的超时事件。 当我们插入第二个超时事件时系统需要逐一判断这个超时事件的time 是否大于超时链表挂载的超时事件time逐一对比之后发送插入的超时事件time 比超时链表挂载的超时事件time 要大则系统把这个超时事件插入这张链表的尾部。如下图所示 如果插入的超时事件time 与挂载超时链表的超时事件time 对比之后发现插入的超时事件time 在两个挂载的超时事件time 之间即atimeb那么要插入的超时事件挂载至这两个挂载的超时事件中间如下图所示 2. 删除超时事件 从超时事件链表中删除一个超时事件可调用sys_untimeout 函数删除如下源码所示 void sys_untimeout(sys_timeout_handler handler, void *arg) {struct sys_timeo *prev_t, *t;/* 从链表头开始遍历这个链表*/for (t next_timeout, prev_t NULL; t ! NULL; prev_t t, t t-next){/* 查找删除的超时事件判断超时事件的回调函数与函数参数是否一致*/if ((t-h handler) (t-arg arg)){if (prev_t NULL){next_timeout t-next;}else{prev_t-next t-next;}memp_free(MEMP_SYS_TIMEOUT, t);return;}}return; } 此函数非常简单只需遍历这个超时链表在遍历过程中判断超时事件的回调函数与函数 参数是否一致若一致则对超时链表的超时事件排序排序完成之后调用memp_free 删除这 个超时事件。 超时定时器检查 不管是OS 的还是裸机的都可以对其进行超时检查和处理lwIP 使用两个函数来实现超时 检查处理。 void sys_check_timeouts(void)函数 这个函数是用于裸机部分的用户可以在裸机的应用中周期性调用该函数每次进来检查 定时器链表上定时最短的定时器是否到期如果没有到期直接退出该函数否则执行该定 时器回调函数并从链表上删除该定时器然后继续检查下一个定时器直到没有一个定时器 到期退出。tcpip_timeouts_mbox_fetch(sys_mbox_t *mbox, void **msg)函数 这个函数在OS 线程中循环执行的主要等待mbox 消息并可阻塞如果等待mbox 时超 时则会同时执行超时事件处理即调用超时回调函数否则一直没有收到mbox 消息就会一 直等待直到下一个超时时间并循环将所有超时定时器检查一遍( 内部调用了void sys_check_timeouts(void))lwIP 中tcpip 线程就是靠这种方法即处理了上层及底层的mbox 消息同时处理了所有需要定时处理的事件。 tcpip_thread协议栈线程 这个线程由tcpip_init 函数创建该函数如下所示 void tcpip_init(tcpip_init_done_fn initfunc, void *arg) {lwip_init();tcpip_init_done initfunc;tcpip_init_done_arg arg;if (sys_mbox_new(tcpip_mbox, TCPIP_MBOX_SIZE) ! ERR_OK){LWIP_ASSERT(failed to create tcpip_thread mbox, 0);} #if LWIP_TCPIP_CORE_LOCKINGif (sys_mutex_new(lock_tcpip_core) ! ERR_OK){LWIP_ASSERT(failed to create lock_tcpip_core, 0);} #endif /* LWIP_TCPIP_CORE_LOCKING */sys_thread_new(TCPIP_THREAD_NAME, tcpip_thread, NULL,TCPIP_THREAD_STACKSIZE, TCPIP_THREAD_PRIO); } 这个函数在lwip_init 函数调用它负责几个任务第一、创建邮箱为数据传输准备第 二、创建互斥锁为防止优先级翻转问题第三、创建TCP/IP 线程。下面笔者重点讲解 tcpip_thread 任务函数的实现源码如下所示 static void tcpip_thread(void *arg) {struct tcpip_msg *msg;LWIP_UNUSED_ARG(arg);LWIP_MARK_TCPIP_THREAD();LOCK_TCPIP_CORE();if (tcpip_init_done ! NULL){tcpip_init_done(tcpip_init_done_arg);}while (1){LWIP_TCPIP_THREAD_ALIVE();/* 第一步等待消息时将在等待时处理超时*//* TCPIP_MBOX_FETCH的宏定义为sys_timeouts_mbox_fetch等待消息并且处理超时事件*/TCPIP_MBOX_FETCH(tcpip_mbox, (void **)msg);if (msg NULL) /* 如果没有等到消息就继续等待*/{continue;}tcpip_thread_handle_msg(msg);} } static void tcpip_thread_handle_msg(struct tcpip_msg *msg) {/* 第二步等待到消息就对消息进行处理*//* 不同类型进行不同的处理*/switch (msg-type){ #if !LWIP_TCPIP_CORE_LOCKING/* 执行对应的API 函数*/case TCPIP_MSG_API:msg-msg.api_msg.function(msg-msg.api_msg.msg);break;case TCPIP_MSG_API_CALL:msg-msg.api_call.arg-err msg-msg.api_call.function(msg-msg.api_call.arg);sys_sem_signal(msg-msg.api_call.sem);break; #endif /* !LWIP_TCPIP_CORE_LOCKING */ #if !LWIP_TCPIP_CORE_LOCKING_INPUT/* 直接交给ARP 层处理*/case TCPIP_MSG_INPKT:if (msg-msg.inp.input_fn(msg-msg.inp.p,msg-msg.inp.netif) ! ERR_OK){pbuf_free(msg-msg.inp.p);}memp_free(MEMP_TCPIP_MSG_INPKT, msg);break; #endif /* !LWIP_TCPIP_CORE_LOCKING_INPUT */ #if LWIP_TCPIP_TIMEOUT LWIP_TIMERS/* 注册一个超时事件*/case TCPIP_MSG_TIMEOUT:sys_timeout(msg-msg.tmo.msecs, msg-msg.tmo.h, msg-msg.tmo.arg);memp_free(MEMP_TCPIP_MSG_API, msg);break;/* 删除一个超时事件*/case TCPIP_MSG_UNTIMEOUT:sys_untimeout(msg-msg.tmo.h, msg-msg.tmo.arg);memp_free(MEMP_TCPIP_MSG_API, msg);break; #endif /* LWIP_TCPIP_TIMEOUT LWIP_TIMERS *//* 通过回调方式执行一个回调函数他们的回调函数相同*/case TCPIP_MSG_CALLBACK:msg-msg.cb.function(msg-msg.cb.ctx);memp_free(MEMP_TCPIP_MSG_API, msg);break;case TCPIP_MSG_CALLBACK_STATIC:msg-msg.cb.function(msg-msg.cb.ctx);break;default:break;} } 协议栈线程主要负责接收邮箱的消息、递交数据至网络层、遍历超时链表等任务。 lwIP 中的消息 在上一个小节笔者讲解了tcpip_thread 线程的作用其中接收邮箱的消息到底如何构建 这里涉及到lwIP 数据包消息机制它专门把ETH 中断接收的数据封装成消息以邮箱的方式 发送至tcpip_thread 线程处理注这里以带操作系统为例。 数据包消息(tcpip_msg) /* 7种tcpip_msg消息类型*/ enum tcpip_msg_type {TCPIP_MSG_API, /* 用户调用应用层的接口时就属于API消息类型*/TCPIP_MSG_API_CALL, /* API 函数调用*/TCPIP_MSG_INPKT, /* 底层数据包输入*/TCPIP_MSG_TIMEOUT, /* 注册超时事件*/TCPIP_MSG_UNTIMEOUT, /* 删除超时事件*/TCPIP_MSG_CALLBACK, /* 执行回调函数*/TCPIP_MSG_CALLBACK_STATIC /* 执行静态回调函数*/ }; /* tcpip_msg结构体*/ struct tcpip_msg {/* tcpip_msg消息的类型*/enum tcpip_msg_type type;/* 消息内容,共用体不同消息类型使用不同的结构*/union{struct{/* 内核执行函数*/tcpip_callback_fn function;/* 执行函数的参数*/void *msg;} api_msg;struct{/* 回调函数*/tcpip_api_call_fn function;/* 回调函数的参数*/struct tcpip_api_call_data *arg;/* 用户同步的信号量*/sys_sem_t *sem;} api_call;struct{/* 接收的数据包*/struct pbuf *p;/* 接收的数据包的网络接口*/struct netif *netif;/* 输入的函数接口*/netif_input_fn input_fn;} inp;struct{/* tcpip回调函数*/tcpip_callback_fn function;/* 回调函数参数*/void *ctx;} cb;struct{/* 超时时间*/u32_t msecs;/* 超时执行的回调函数*/sys_timeout_handler h;/* 传入超时回调函数的形参*/void *arg;} tmo; #endif /* LWIP_TCPIP_TIMEOUT LWIP_TIMERS */} msg; }; 上述的源码中我们可以看到消息结构的msg 字段是一个共用体union共用体中定义了 各类型消息的具体内容每种类型的消息对应了共用体中的一个字段其中注册超时事件和删 除超时事件消息共用一个tmo 结构体回调事件与静态回调事件消息也共用一个cb 结构体 API 调用与NETIF 的API 调用相关的消息具体内容比较多不宜直接放在tcpip_msg 中系统 用了专门的结构体api_msg 来描述对应消息的具体内容。注tcpip_msg 中只保存了一个指向 api_msg 指针。 tcpip_thread 线程处理每种类型的消息时lwIP 内核就会产生与之对应的消息函数首先 产生的消息传递到系统邮箱tcpip_mboxtcpip_thread 线程需要判断该消息的类型从而做 出相应的处理在图7.1.1 中笔者大概描述了lwIP 接收数据的流程图直观上它是通过函数 tcpip_input 对消息进行构造和投递当然该函数真正执行的是函数tcpip_inpkt如下源码所示 err_t tcpip_input(struct pbuf *p, struct netif *inp) { #if LWIP_ETHERNETif (inp-flags (NETIF_FLAG_ETHARP | NETIF_FLAG_ETHERNET)){/* 把ethernet_input()作用该函数的一部分内核接收到这个数据包就调用该函数*/return tcpip_inpkt(p, inp, ethernet_input);}else #endif /* LWIP_ETHERNET */return tcpip_inpkt(p, inp, ip_input); } err_t tcpip_inpkt(struct pbuf *p, struct netif *inp, netif_input_fn input_fn) { #if LWIP_TCPIP_CORE_LOCKING_INPUTerr_t ret;ret input_fn(p, inp);return ret; #else /* LWIP_TCPIP_CORE_LOCKING_INPUT */struct tcpip_msg *msg;msg (struct tcpip_msg *)memp_malloc(MEMP_TCPIP_MSG_INPKT);if (msg NULL){return ERR_MEM;}msg-type TCPIP_MSG_INPKT;msg-msg.inp.p p; /* 指向pbuf数据包*/msg-msg.inp.netif inp; /* 网络接口*//* 构造消息消息的类型是数据包消息处理函数是ethernet_input() */msg-msg.inp.input_fn input_fn;if (sys_mbox_trypost(mbox, msg) ! ERR_OK) /* 构造消息完成发送邮箱*/{memp_free(MEMP_TCPIP_MSG_INPKT, msg); /* 释放内存池*/return ERR_MEM;}return ERR_OK; #endif /* LWIP_TCPIP_CORE_LOCKING_INPUT */ } 总的来说接收数据都是通过函数ethernet_input当然无操作系统也是如此只不过就 是传递消息的方式不同无操作系统一般使用回调函数传递消息而操作系统一般使用IPC 通信机制例如邮箱信号量等通信机制lwIP 的IPC 通讯示意图如下图所示 API 消息 所谓API 消息其实就是两个API 部分的交互的消息它是由用户调用API 函数为起点 使用IPC 通信机制告诉内核需要执行那个部分的API 函数内核的具体消息内容都可以直接 包含内核消息结构tcpip_msg但是API 消息除外由于它的消息内容实在庞大所以协议栈 专门用结构体api_msg 来描述API 消息内容而在tcpip_msg 结构体中只维护该类型的指针 前面笔者也讲解到tcpip_msg 时候它里面包含了一个api_msg 指针这个指针只是指向 api_msg 结构体现在的api_msg 结构体在api_msg.h 文件定义的该结构体如下所示 struct api_msg {struct netconn *conn; /* 当前连接*/err_t err; /* 返回结果*/union{/* 用于函数lwip_netconn_do_send()参数*/struct netbuf *b;/* 用于函数lwip_netconn_do_newconn()参数*/struct{u8_t proto;} n;/* 用于函数lwip_netconn_do_bind()和函数lwip_netconn_do_connect()参数*/struct{API_MSG_M_DEF_C(ip_addr_t, ipaddr); /* ip 地址*/u16_t port; /* 端口号*/u8_t if_idx;} bc;/* 用于函数lwip_netconn_do_getaddr()参数*/struct{ip_addr_t API_MSG_M_DEF(ipaddr); /* ip 地址*/u16_t API_MSG_M_DEF(port); /* 端口号*/u8_t local;} ad;/* 用于函数lwip_netconn_do_write()参数*/struct{/** 当前要写的向量e */const struct netvector *vector;/** 未写向量的个数*/u16_t vector_cnt;/** 偏移成矢量*/size_t vector_off;/** 向量的总长度*/size_t len;/** 当err ERR_OK时写入的字节的总长度/输出的偏移量*/size_t offset;u8_t apiflags; #if LWIP_SO_SNDTIMEOu32_t time_started; #endif /* LWIP_SO_SNDTIMEO */} w;/** 用于函数lwip_netconn_do_recv()参数*/struct{u32_t len;} r; #if LWIP_TCP/* 用于函数wip_netconn_do_close (/shutdown)参数*/struct{u8_t shut; #if LWIP_SO_SNDTIMEO || LWIP_SO_LINGERu32_t time_started; #else /* LWIP_SO_SNDTIMEO || LWIP_SO_LINGER */u8_t polls_left; #endif /* LWIP_SO_SNDTIMEO || LWIP_SO_LINGER */} sd; #endif /* LWIP_TCP */ #if LWIP_IGMP || (LWIP_IPV6 LWIP_IPV6_MLD)/* 用于函数lwip_netconn_do_join_leave_group()参数*/struct{API_MSG_M_DEF_C(ip_addr_t, netif_addr);u8_t if_idx;enum netconn_igmp join_or_leave;} jl; #endif /* LWIP_IGMP || (LWIP_IPV6 LWIP_IPV6_MLD) */ #if TCP_LISTEN_BACKLOGstruct{u8_t backlog;} lb; #endif /* TCP_LISTEN_BACKLOG */} msg; #if LWIP_NETCONN_SEM_PER_THREADsys_sem_t *op_completed_sem; #endif /* LWIP_NETCONN_SEM_PER_THREAD */ }; 这个结构体只包含了三个字段分别为描述连接信息的conn、内核返回的执行结果err、 以及msg。在api_msg 结构体中保存conn 字段是必须的因为conn 结构中包含了与该连接相 关的邮箱和信号量等信息协议栈进程要用这些信息来完成与应用进程间的同步与通信共用 体类型msg 的各个成员与调用它的函数密切相关如lwip_netconn_do_xxxxxx 表示不一样的NETCONN 的API 接口类型的函数执行需要用这些信息来完成与应用线程的通信与同步 内核执行lwip_netconn_do_xxx 类型的函数返回结果会被记录在err 中msg 的各个参数记录各 个函数执行时需要的详细参数。 到了这里我们已经理解了底层数据包消息同理API 函数的调用也是如此如果用户 要与内核进行数据传递也是需要lwIP 的消息机制毕竟用户和内核都是独立的线程或者任 务例如我们使用SOCKET 的API 接口和NETCONN 的API 接口时候lwIP 会把用户调用的 函数与参数做成消息传递给tcpip_thraed 线程这个消息就是lwIP 中的API 消息lwIP 为什么 会使用这些方式呢首先对于用户来说不需要很深入的理解lwIP 内核只需要调用API 函 数接口就可以完成实验例如在NETCONN 的API 中构造数据包时就会调用 netconn_apimsg 函数进行投递消息。 其实lwIP 的协议栈API 实现有两个部分组成一部分为用户编程接口函数提供给用户 这些函数在用户进程执行另一部分驻留在内核进程这两个部分的通信方法是使用IPC 通 信机制被用到的进程通信机制有以下四种 邮箱用于数据交互。信号量用于用户和系统同步。互斥信号量用于优先级翻转的问题。共享内存内核消息结构tcpip_msg 和API 消息内容api_msg。 根据上述的通信机制我们可以得到用户调用函数与内核进程的关系图如下图所示 图7.4.2.1 用户API 与内核进程的关系图 接下来笔者就以NETCONN 的API 为例来讲解lwIP 的API 消息使用如下源码所示 err_t netconn_bind(struct netconn *conn, const ip_addr_t *addr, u16_t port) {/*声明api_msg消息结构体*/API_MSG_VAR_DECLARE(msg);err_t err; #if LWIP_IPV4if (addr NULL){addr IP4_ADDR_ANY;} #endif /* LWIP_IPV4 *//* 第一步构建api_msg结构体*/API_MSG_VAR_ALLOC(msg); /* 申请内存*//* 连接的信息*/API_MSG_VAR_REF(msg).conn conn;/* IP地址*/API_MSG_VAR_REF(msg).msg.bc.ipaddr API_MSG_VAR_REF(addr);/* 端口号*/API_MSG_VAR_REF(msg).msg.bc.port port;/* 发送API消息并等待信号量*/err netconn_apimsg(lwip_netconn_do_bind, API_MSG_VAR_REF(msg));API_MSG_VAR_FREE(msg);return err; } static err_t netconn_apimsg(tcpip_callback_fn fn, struct api_msg *apimsg) {err_t err;/* 发送API消息并等待信号量*/err tcpip_send_msg_wait_sem(fn, apimsg, LWIP_API_MSG_SEM(apimsg));if (err ERR_OK){return apimsg-err; /* 返回API的错误码*/}return err; } err_t tcpip_send_msg_wait_sem(tcpip_callback_fn fn, void *apimsg, sys_sem_t *sem) { #if LWIP_TCPIP_CORE_LOCKINGLWIP_UNUSED_ARG(sem);LOCK_TCPIP_CORE();fn(apimsg);UNLOCK_TCPIP_CORE();return ERR_OK; #else /* LWIP_TCPIP_CORE_LOCKING *//*声明tcpip_msg消息结构体*/TCPIP_MSG_VAR_DECLARE(msg);/* 第二步构造tcpip_msg消息*/TCPIP_MSG_VAR_ALLOC(msg); /* 申请内存*/TCPIP_MSG_VAR_REF(msg).type TCPIP_MSG_API; /* 消息类型*/TCPIP_MSG_VAR_REF(msg).msg.api_msg.function fn; /* 设置回调函数*/TCPIP_MSG_VAR_REF(msg).msg.api_msg.msg apimsg; /* 指向api_msg消息*//* 第三步释放邮箱*/sys_mbox_post(mbox, TCPIP_MSG_VAR_REF(msg));/* 第四步等待信号量*/sys_arch_sem_wait(sem, 0);TCPIP_MSG_VAR_FREE(msg);return ERR_OK; #endif /* LWIP_TCPIP_CORE_LOCKING */ } 上述的源码我们可分为四步讲解它们分别构造tcpip_msg、api_msg、发送邮箱消息和 等待信号量如下图所示 其实上图并不是lwIP 的最优先的流程图因为在函数tcpip_send_msg_wait_sem 中宏定义 LWIP_TCPIP_CORE_LOCKING 是为1 的表示无需操作系统的邮箱与信号量参与在该函数 只执行以下源码 err_t tcpip_send_msg_wait_sem(tcpip_callback_fn fn, void *apimsg, sys_sem_t *sem) { #if LWIP_TCPIP_CORE_LOCKINGLWIP_UNUSED_ARG(sem);LOCK_TCPIP_CORE();fn(apimsg);UNLOCK_TCPIP_CORE();return ERR_OK; #else /* LWIP_TCPIP_CORE_LOCKING */ /* 代码省略*/ #endif /* LWIP_TCPIP_CORE_LOCKING */ } 上述函数也非常简单理解首先此函数调用LOCK_TCPIP_CORE 函数上锁记者系统直 接调用函数lwip_netconn_do_bind 对api_msg 消息做处理这样的方法省去了tcpip_mag 消息 构建、邮箱以及信号量等操作。如下图所示 RAW 编程接口UDP 实验 本章我们一起学习传输层UDP 协议与RAW API 编程接口总的来说本章节的内容会 涉及到传输层和应用层的知识。 UDP 协议简介 UDP 协议是TCP/IP 协议栈的传输层协议是一个简单的面向数据报的协议在传输层中 还有另一个重要的协议那就是TCP 协议TCP 协议的知识笔者会在下一章节中讲解。UDP 不提供数据包分组、组装不能对数据包进行排序当报文发送出去后无法知道是否安全、完 整的到达。UDP 除了这些缺点外肯定有它自身的优势由于UDP 不属于连接型协议因而消 耗资源小处理速度快所以通常在音频、视频和普通数据传输时使用UDP 较多。UDP 数据 报结构如下图所示。 UDP 首部有8 个字节由4 个字段构成每个字段都是两个字节这些字段的作用如下 ①源端口源端口号需要对方回信时选用不需要时全部置0。 ②目的端口目的端口号在终点交付报文的时候需要用到。 ③长度UDP 的数据报的长度包括首部和数据其最小值为8只有首部。 ①校验和检测UDP 数据报在传输中是否有错有错则丢弃。 UDP 协议使用端口号为不同的应用保留各自的数据传输通道UDP 和TCP 协议都是采用 端口号对同一时刻内多项应用同时发送和接收数据而数据接收方则通过目标端口接收数据。 有的网络应用只能使用预先为其预留或注册的静态端口而另外一些网络应用则可以使用未被 注册的动态端口。因为UDP 报头使用两个字节存放端口号所以端口号的有效范围是从0 到 65535。一般来说大于49151 的端口号都代表动态端口。 数据报的长度是指包括报头和数据部分在内的总字节数。因为报头的长度是固定的所以 该数据区域主要被用来计算可变长度的数据部分又称为数据负载。数据报的最大长度根据 操作环境的不同而各异。从理论上说包含报头在内的数据报的最大长度为65535 字节。 UDP 协议使用报头中的校验和来保证数据的安全。校验和首先在数据发送方通过特殊的 算法计算得出在传递到接收方之后还需要再重新计算。如果某个数据报在传输过程中被第 三方篡改或者由于线路噪音等原因受到损坏发送和接收方的校验计算和将不会相符由此 UDP 协议可以检测是否出错。 UDP 报文封装流程 UDP 报文与TCP 报文一样也是由UDP/TCP 首部数据区域组成UDP 协议是位于传输层 该层是应用层的下一层当用户发送数据时候需要选择使用那种协议发送出去如果使用 UDP 协议则UDP 协议就会简单的把数据封装起来UDP 报文结构如下图所示 UDP 报文的数据结构 (1) UDP 首部结构 从上面可知UDP 首部包含了四个字段这些字段在lwIP 内核中由结构体udp_hdr 描述 该结构体如下所示 struct udp_hdr {PACK_STRUCT_FIELD(u16_t src); /* 源端口*/PACK_STRUCT_FIELD(u16_t dest); /* 目的端口*/PACK_STRUCT_FIELD(u16_t len); /* 长度*/PACK_STRUCT_FIELD(u16_t chksum); /* 校验和*/ } PACK_STRUCT_STRUCT; 可见这个结构体的成员变量与图11.1.1 的UDP 首部字段一一对应。 (2) UDP 控制块 lwIP 为了更好的管理UDP 报文它定义了一个UDP 控制块使用该控制块来记录UDP 的通讯信息例如源端口、目的端口源IP 地址和目的IP 地址以及收到的数据回调函数等信 息lwIP 把多个UDP 控制块使用链表形式连接起来在处理时候遍历列表即可该UDP 控制 块结构如以下所示 #define IP_PCB \ip_addr_t local_ip; \\/* 本地ip 地址与远端IP 地址*/ ip_addr_t remote_ip; u8_t netif_idx; \ /* 绑定netif 索引*/u8_t so_options; \ /* Socket选项*/u8_t tos; \ /* 服务类型*/u8_t ttl \ /* 生存时间*/IP_PCB_NETIFHINT /* 链路层地址解析提示*/struct ip_pcb {IP_PCB; }; struct udp_pcb {IP_PCB;struct udp_pcb *next; /* 指向下一个控制块*/u8_t flags; /* 控制块状态*/u16_t local_port, remote_port; /* 本地端口和目标端口*/udp_recv_fn recv; /* 接收回调函数*/void *recv_arg; /* 用户为recv回调提供的参数*/ }; 可以看到结构体udp_pcb 包含了指向下一个节点的指针next多个UDP 控制块构建了 一个单向链表且各个控制块指向独立的接收回调函数如下图所示 对于RAW 的API 接口来讲上图中的recv 由用户提供这个函数而NETCONN 和 SOCKET 接口无需用户提供回调函数因为lwIP 内核已经注册了该回调函数所以数据到来 时该函数把数据以邮箱的方式发送至NETCONN 和SOCKET 对应的接口。 11.1.3 发送UDP 报文 UDP 报文发送函数是由udp_sendto_if_src 实现其实它最终调用ip_output_if_src 函数把 数据报递交给网络层处理udp_sendto_if_src 函数如下所示 err_t udp_sendto_if_src(struct udp_pcb *pcb, /* udp控制块*/struct pbuf *p, /* pbuf网络数据包*/const ip_addr_t *dst_ip, /* 目的IP地址*/u16_t dst_port, /* 目的端口*/struct netif *netif, /* 网卡信息*/const ip_addr_t *src_ip) /* 源IP地址*/ {struct udp_hdr *udphdr;err_t err;struct pbuf *q;u8_t ip_proto;u8_t ttl;/* 第一步判断控制块是否为空和远程IP地址是否为空*/if (!IP_ADDR_PCB_VERSION_MATCH(pcb, src_ip) ||!IP_ADDR_PCB_VERSION_MATCH(pcb, dst_ip)){return ERR_VAL; /* 放回错误*/}/* 如果PCB还没有绑定到一个端口那么在这里绑定它*/if (pcb-local_port 0){err udp_bind(pcb, pcb-local_ip, pcb-local_port);if (err ! ERR_OK){return err;}}/* 判断添加UDP首部会不会溢出*/if ((u16_t)(p-tot_len UDP_HLEN) p-tot_len){return ERR_MEM;}/* 第二步没有足够的空间将UDP 首部添加到给定的pbuf 中*/if (pbuf_add_header(p, UDP_HLEN)){/* 在单独的新pbuf中分配标头*/q pbuf_alloc(PBUF_IP, UDP_HLEN, PBUF_RAM);/* 在单独的新pbuf中分配标头*/if (q NULL){return ERR_MEM; /* 返回错误*/}if (p-tot_len ! 0){/* 把首部pbuf 和数据pbuf 连接到一个pbuf 链表上*/pbuf_chain(q, p);}}else /* 如果有足够的空间*/{/* 在数据pbuf 中已经预留UDP 首部空间*//* q 指向pbuf */q p;}/* 第三步设置UDP首部信息*//* 指向它的UDP首部*/udphdr (struct udp_hdr *)q-payload;/* 填写本地IP端口*/udphdr-src lwip_htons(pcb-local_port);/* 填写目的端口*/udphdr-dest lwip_htons(dst_port);/* 填写校验和*/udphdr-chksum 0x0000;/* 设置长度*/udphdr-len lwip_htons(q-tot_len);/* 设置协议类型*/ip_proto IP_PROTO_UDP;/* 设置生存时间*/ttl pcb-ttl;/* 第四步发送到IP 层*/NETIF_SET_HWADDRHINT(netif, (pcb-addr_hint));err ip_output_if_src(q, src_ip, dst_ip, ttl, pcb-tos, ip_proto, netif);NETIF_SET_HWADDRHINT(netif, NULL);MIB2_STATS_INC(mib2.udpoutdatagrams);if (q ! p){/*释放内存*/pbuf_free(q);q NULL;}UDP_STATS_INC(udp.xmit);return err; } 此函数非常简单首先判断源IP 地址和目标IP 地址是否为空接着判断本地端口是否为 空判断完成之后添加UDP 首部最后调用ip_output_if_src 函数把数据报递交给网络层处理。 UDP 报文接收 网络层处理数据报完成之后由udp_input 函数把数据报递交给传输层该函数源码所示 void udp_input(struct pbuf *p, struct netif *inp) {struct udp_hdr *udphdr;struct udp_pcb *pcb, *prev;struct udp_pcb *uncon_pcb;u16_t src, dest;u8_t broadcast;u8_t for_us 0;LWIP_UNUSED_ARG(inp);PERF_START;UDP_STATS_INC(udp.recv);/* 第一步判断数据报长度少于UDP首部*/if (p-len UDP_HLEN){UDP_STATS_INC(udp.lenerr);UDP_STATS_INC(udp.drop);MIB2_STATS_INC(mib2.udpinerrors);pbuf_free(p); /* 释放内存掉弃该数据报*/goto end;}/* 指向UDP首部*/udphdr (struct udp_hdr *)p-payload;/* 判断是否是广播包*/broadcast ip_addr_isbroadcast(ip_current_dest_addr(), ip_current_netif());/* 得到源端口号*/src lwip_ntohs(udphdr-src);/* 得到目的端口号*/dest lwip_ntohs(udphdr-dest);udp_debug_print(udphdr);pcb NULL;prev NULL;uncon_pcb NULL;/* 第二步遍历UDP pcb列表以找到匹配的pcb */for (pcb udp_pcbs; pcb ! NULL; pcb pcb-next){/* 第三步比较PCB本地IP地址与端口*/if ((pcb-local_port dest) (udp_input_local_match(pcb, inp, broadcast) ! 0)){/* 判断UDP控制块的状态*/if (((pcb-flags UDP_FLAGS_CONNECTED) 0) ((uncon_pcb NULL))){/* 如果未找到使用第一个UDP 控制块*/uncon_pcb pcb;}/* 判断目的IP是否为广播地址*/else if (broadcast ip4_current_dest_addr()-addr IPADDR_BROADCAST){/* 全局广播地址(仅对IPv4有效;之前检查过匹配)*/if (!IP_IS_V4_VAL(uncon_pcb-local_ip) || !ip4_addr_cmp(ip_2_ip4(uncon_pcb-local_ip),netif_ip4_addr(inp))){/* 检查此pcb uncon_pcb与输入netif不匹配*/if (IP_IS_V4_VAL(pcb-local_ip) ip4_addr_cmp(ip_2_ip4(pcb-local_ip),netif_ip4_addr(inp))){/* 更好的匹配*/uncon_pcb pcb;}}}/* 比较PCB远程地址端口和UDP源地址端口*/if ((pcb-remote_port src) (ip_addr_isany_val(pcb-remote_ip) ||ip_addr_cmp(pcb-remote_ip, ip_current_src_addr()))){/* 第一个完全匹配的PCB */if (prev ! NULL){/* 将pcb移到udp_pcbs前面*/prev-next pcb-next;pcb-next udp_pcbs;udp_pcbs pcb;}else{UDP_STATS_INC(udp.cachehit);}break;}}prev pcb;}/* 第五步找不到完全匹配的UDP 控制块将第一个未使用的UDP 控制块作为匹配结果*/if (pcb NULL){pcb uncon_pcb;}/* 检查校验和是否匹配或是否匹配*/if (pcb ! NULL){for_us 1;}else{ #if LWIP_IPV4if (!ip_current_is_v6()){for_us ip4_addr_cmp(netif_ip4_addr(inp), ip4_current_dest_addr());} #endif /* LWIP_IPV4 */}/* 第六步如果匹配*/if (for_us){/* 调整报文的数据区域指针*/if (pbuf_header(p, -UDP_HLEN)){UDP_STATS_INC(udp.drop);MIB2_STATS_INC(mib2.udpinerrors);pbuf_free(p);goto end;}/* 如果找到对应的控制块*/if (pcb ! NULL){MIB2_STATS_INC(mib2.udpindatagrams);/* 回调函数将数据递交给上层应用*/if (pcb-recv ! NULL){/* 回调函数recv 需要负责释放p */pcb-recv(pcb-recv_arg, pcb, p, ip_current_src_addr(), src);}else{/* 如果recv 函数没有注册直接释放p */pbuf_free(p);goto end;}}else /* 第七步没有找到匹配的控制块返回端口不可达ICMP 报文*/{if (!broadcast !ip_addr_ismulticast(ip_current_dest_addr())){/* 将数据区域指针移回IP 数据报首部*/pbuf_header_force(p, (s16_t)(ip_current_header_tot_len() UDP_HLEN));/* 返回一个端口不可达ICMP 差错控制报文到源主机中*/icmp_port_unreach(ip_current_is_v6(), p);}UDP_STATS_INC(udp.proterr);UDP_STATS_INC(udp.drop);MIB2_STATS_INC(mib2.udpnoports);pbuf_free(p); /* 掉弃该数据包*/}}/* 如果不匹配则掉弃该数据包*/else{pbuf_free(p);} end:PERF_STOP(udp_input);return; } 可以看出此函数根据接收数据包的UDP 首部信息遍历UDP 控制块链表找到对应的控 制块之后lwIP 内核把接收到的数据包递交给pcb-recv 回调函数处理。 RAW 的UDP 接口简介 下表给出了UDP 协议的RAW 的API 功能函数我们使用这些函数来完成UDP 的数据发 送和接收功能。 表11.2.1 只是列出了我们在编程时需要使用到的函数接下来我们来看一下上述表中重要 的函数 (1) udp_new 函数 此函数用来创建一个UDP 控制块这个控制块用来描述IP 地址、端口号和状态等信息 该函数实现源码如下所示 struct udp_pcb * udp_new(void) {struct udp_pcb *pcb;/* 申请一个UDP内存池*/pcb (struct udp_pcb *)memp_malloc(MEMP_UDP_PCB);/* 申请成功*/if (pcb ! NULL){/* 初始化PCB控制块所有零*/memset(pcb, 0, sizeof(struct udp_pcb));/* pcb-ttl 255 */pcb-ttl UDP_TTL;}return pcb; } 可以看到该控制块的内存由内存池申请申请成功之后设置该控制块的生存时间。 (2) udp_remove 函数 从PCB 控制块链表中移除一个控制块并且把移除的控制块释放内存该函数实现源码 如下所示 void udp_remove(struct udp_pcb *pcb) {struct udp_pcb *pcb2;mib2_udp_unbind(pcb);/* 判断pcb被删除在列表的第一个*/if (udp_pcbs pcb){/* 从第二pcb开始制作列表*/udp_pcbs udp_pcbs-next;}else /* pcb不在列表的第一个*/{/* 遍历pcb列表*/for (pcb2 udp_pcbs; pcb2 ! NULL; pcb2 pcb2-next){/* 在udp_pcbs列表中查找pcb */if (pcb2-next ! NULL pcb2-next pcb){/* 从列表中删除pcb */pcb2-next pcb-next;break;}}}memp_free(MEMP_UDP_PCB, pcb); } 以传入的控制块为条件遍历PCB 控制块链表若链表中的控制块等于要移除的控制块则该控制块移除PCB 控制块链表移除完成之后释放该控制块的内存。 (3) udp_recv 函数 此函数用来设置接收回调函数及函数参数若用户使用RAW 接口实现UDP则用户必须 调用此函数设置接收回调函数该函数的源码如下所示 void udp_recv(struct udp_pcb *pcb, udp_recv_fn recv, void *recv_arg) {/* 调用recv()回调和用户数据*/pcb-recv recv;pcb-recv_arg recv_arg; } 可以看出设置的函数和形参都是由UDP 控制块的字段指向。 RAW 的UDP 实验 硬件设计 例程功能 本章实验的目标是PC 端和开发板通过UDP 协议连接起来PC 端使用网络调试助手向开 发板发送数据开发板接收到以后在LCD 上显示接收到的数据我们也可以通过开发板上的 按键发送数据给PC。 该实验的实验工程请参考《lwIP 例程2 lwIP_RAW_UDP 实验》。 软件设计 11.3.2.1 UDP 配置步骤 创建UDP 控制块 调用函数udp_new 创建UDP 控制块。连接指定的IP 地址和端口号 调用函数udp_connect 绑定远程IP 地址和远程端口号。绑定本地IP 地址与端口号 调用函数udp_bind 绑定本地IP 地址和本地端口号。注册接收回调函数 udp_recv 是注册接收回调函数该函数需要自己编写。发送数据 调用函数udp_send 发送数据。 11.3.2.2 程序流程图 本实验的程序流程图如下图所示 程序解析 本实验重点讲解lwip_deom.c 这个文件该文件定义了五个函数它们的作用如下表所示 我们首先看一下lwip_demo 函数该函数的代码如下。 /*** brief lwip_demo实验入口* param 无* retval 无*/ void lwip_demo(void) {err_t err;struct udp_pcb *udppcb; /* 定义一个TCP服务器控制块*/ip_addr_t rmtipaddr; /* 远端ip地址*/char *tbuf;uint8_t key;uint8_t res 0;uint8_t t 0;lwip_demo_set_remoteip(); /* 先选择IP */lcd_clear(BLACK); /* 清屏*/g_point_color WHITE;lcd_show_string(30, 30, 200, 16, 16, STM32, g_point_color);lcd_show_string(30, 50, 200, 16, 16, UDP Test, g_point_color);lcd_show_string(30, 70, 200, 16, 16, ATOM正点原子, g_point_color);lcd_show_string(30, 90, 200, 16, 16, KEY0:Send data, g_point_color);tbuf mymalloc(SRAMIN, 200); /* 申请内存*/if (tbuf NULL)return; /* 内存申请失败了,直接退出*/sprintf((char *)tbuf, Local IP:%d.%d.%d.%d, lwipdev.ip[0],lwipdev.ip[1],lwipdev.ip[2],lwipdev.ip[3]); /* 服务器IP */lcd_show_string(30, 130, 210, 16, 16, tbuf, g_point_color);/* 远端IP */sprintf((char *)tbuf, Remote IP:%d.%d.%d.%d, lwipdev.remoteip[0],lwipdev.remoteip[1],lwipdev.remoteip[2],lwipdev.remoteip[3]);lcd_show_string(30, 150, 210, 16, 16, tbuf, g_point_color);sprintf((char *)tbuf, Remote Port:%d, UDP_DEMO_PORT); /* 客户端端口号*/lcd_show_string(30, 170, 210, 16, 16, tbuf, g_point_color);g_point_color BLUE;lcd_show_string(30, 190, 210, 16, 16, STATUS:Disconnected, g_point_color);udppcb udp_new();if (udppcb) /* 创建成功*/{IP4_ADDR(rmtipaddr, lwipdev.remoteip[0], lwipdev.remoteip[1],lwipdev.remoteip[2], lwipdev.remoteip[3]);/* UDP客户端连接到指定IP地址和端口号的服务器*/err udp_connect(udppcb, rmtipaddr, UDP_DEMO_PORT);if (err ERR_OK){/* 绑定本地IP地址与端口号*/err udp_bind(udppcb, IP_ADDR_ANY, UDP_DEMO_PORT);if (err ERR_OK) /* 绑定完成*/{udp_recv(udppcb, lwip_demo_callback, NULL); /* 注册接收回调函数*//* 标记连接上了(UDP是非可靠连接,这里仅仅表示本地UDP已经准备好) */lcd_show_string(30, 190, 210, 16, 16, STATUS:Connected ,g_point_color);g_point_color WHITE;lcd_show_string(30, 210, lcddev.width - 30,lcddev.height - 190, 16,Receive Data:, g_point_color); /* 提示消息*/g_point_color BLUE;}elseres 1;}elseres 1;}elseres 1;while (res 0){key key_scan(0);if (key KEY1_PRES)break;if (key KEY0_PRES) /* KEY0按下了,发送数据*/{lwip_demo_senddata(udppcb);}if (lwip_send_flag 1 6) /* 是否收到数据*/{/* 清上一次数据*/lcd_fill(30, 230, lcddev.width - 1, lcddev.height - 1, BLACK);/* 显示接收到的数据*/lcd_show_string(30, 230, lcddev.width - 30, lcddev.height - 230, 16,(char *)udp_demo_recvbuf, g_point_color);lwip_demo_flag ~(1 6); /* 标记数据已经被处理了*/}lwip_periodic_handle();delay_ms(2);t;if (t 200){t 0;LED0_TOGGLE();}}lwip_demo_connection_close(udppcb);myfree(SRAMIN, tbuf); } 此函数非常简单它首先设置IP 地址等信息接着调用RAW 相关API 函数配置UDP 连 接值得注意的是UDP 的接收函数由用户编写并且调用udp_recv 函数注册到UDP 控制块 当中。 设置远程IP 地址的函数为lwip_udp_set_remoteip如下源码所示 /*** brief 设置远端IP地址* param 无* retval 无*/ void lwip_udp_set_remoteip(void) {char *tbuf;uint16_t xoff;uint8_t key;lcd_clear(BLACK);g_point_color WHITE;lcd_show_string(30, 30, 200, 16, 16, STM32, g_point_color);lcd_show_string(30, 50, 200, 16, 16, UDP Test, g_point_color);lcd_show_string(30, 70, 200, 16, 16, Remote IP Set, g_point_color);lcd_show_string(30, 90, 200, 16, 16, KEY0: KEY2:-, g_point_color);lcd_show_string(30, 110, 200, 16, 16, KEY1:OK, g_point_color);tbuf mymalloc(SRAMIN, 100); /* 申请内存*/if (tbuf NULL)return;/* 前三个IP保持和DHCP得到的IP一致*/lwipdev.remoteip[0] lwipdev.ip[0];lwipdev.remoteip[1] lwipdev.ip[1];lwipdev.remoteip[2] lwipdev.ip[2];/* 远端IP */sprintf((char *)tbuf, Remote IP:%d.%d.%d., lwipdev.remoteip[0],lwipdev.remoteip[1], lwipdev.remoteip[2]);lcd_show_string(30, 150, 210, 16, 16, tbuf, g_point_color);g_point_color BLUE;xoff strlen((char *)tbuf) * 8 30;lcd_show_xnum(xoff, 150, lwipdev.remoteip[3], 3, 16, 0, g_point_color);while (1){key key_scan(0);if (key KEY1_PRES)break;else if (key){if (key KEY0_PRES)lwipdev.remoteip[3]; /* IP增加*/if (key KEY2_PRES)lwipdev.remoteip[3]--; /* IP减少*//* 显示新IP */lcd_show_xnum(xoff, 150, lwipdev.remoteip[3], 3, 16, 0X80,g_point_color);}}myfree(SRAMIN, tbuf); } 可见此函数根据开发板上的KEY0 和KEY2 按键来设置远程IP 地址设置好IP 地址之 后按下KEY1 退出设置界面。 函数udp_recv 为注册接收回调函数lwip_udp_callback如下源码所示 /*** brief UDP服务器回调函数* param arg 传入参数* param upcbUDP控制块* param p : 网络数据包* param addrIP地址* param port端口号* retval 无*/ static void lwip_udp_callback(void *arg, struct udp_pcb *upcb, struct pbuf *p,const ip_addr_t *addr, u16_t port) {uint32_t data_len 0;struct pbuf *q;if (p ! NULL) /* 接收到不为空的数据时*/{memset(udp_demo_recvbuf, 0, UDP_DEMO_RX_BUFSIZE); /* 数据接收缓冲区清零*/for (q p; q ! NULL; q q-next) /* 遍历完整个pbuf链表*/{/* 判断要拷贝到UDP_DEMO_RX_BUFSIZE中的数据是否大于UDP_DEMO_RX_BUFSIZE的剩余空间如果大于*//* 的话就只拷贝UDP_DEMO_RX_BUFSIZE中剩余长度的数据否则的话就拷贝所有的数据*//* 拷贝数据*/if (q-len (UDP_DEMO_RX_BUFSIZE - data_len))memcpy(udp_demo_recvbuf data_len, q-payload,(UDP_DEMO_RX_BUFSIZE - data_len));elsememcpy(udp_demo_recvbuf data_len, q-payload, q-len);data_len q-len;/* 超出TCP客户端接收数组,跳出*/if (data_len UDP_DEMO_RX_BUFSIZE)break;}upcb-remote_ip *addr; /* 记录远程主机的IP地址*/upcb-remote_port port; /* 记录远程主机的端口号*/lwipdev.remoteip[0] upcb-remote_ip.addr 0xff; /* IADDR4 */lwipdev.remoteip[1] (upcb-remote_ip.addr 8) 0xff; /* IADDR3 */lwipdev.remoteip[2] (upcb-remote_ip.addr 16) 0xff; /* IADDR2 */lwipdev.remoteip[3] (upcb-remote_ip.addr 24) 0xff; /* IADDR1 */udp_demo_flag | 1 6; /* 标记接收到数据了*/pbuf_free(p); /* 释放内存*/}else{udp_disconnect(upcb);lcd_clear(BLACK); /* 清屏*/lcd_show_string(30, 30, 200, 16, 16, STM32, g_point_color);lcd_show_string(30, 50, 200, 16, 16, UDP Test, g_point_color);lcd_show_string(30, 70, 200, 16, 16, ATOM正点原子, g_point_color);lcd_show_string(30, 90, 200, 16, 16, KEY1:Connect, g_point_color);lcd_show_string(30, 190, 210, 16, 16, Connect break, g_point_color);} } 之前笔者讲解过使用RAW 接口实现远程通讯的话它的接收函数由用户编写且调用 udp_recv 函数让控制块的recv 函数指针指向因为lwIP 内核接收到的数据会递交给这个接收 函数所以不注册该函数就无法接收到数据。 lwip_udp_senddata 函数是用来发送数据的在发送数据前我们先通过pbuf_alloc 函数申请 内存当内存申请成功以后我们将发送缓冲区lwip_demo_sendbuf 的首地址填入到ptr 的 payload 字段然后调用udp_send 函数将数据发送出去最后释放申请到的内存代码如下。 /*** brief UDP服务器发送数据* param upcb: UDP控制块* retval 无*/ void lwip_udp_senddata(struct udp_pcb *upcb) {struct pbuf *ptr;/* 申请内存*/ptr pbuf_alloc(PBUF_TRANSPORT, strlen((char *)udp_demo_sendbuf),PBUF_POOL);if (ptr){pbuf_take(ptr, (char *)udp_demo_sendbuf, strlen((char *)udp_demo_sendbuf));/* 将tcp_demo_sendbuf中的数据打包进pbuf结构中*/udp_send(upcb, ptr); /* udp发送数据*/pbuf_free(ptr); /* 释放内存*/} } lwip_demo_connection_close 函数是用来关闭UDP 连接的这个函数很简单通过调用函 数udp_disconnect 来关闭连接然后调用udp_remove 函数将当前被关闭的连接控制块从当前 连接控制块链表中删除代码如下。 /*** brief 关闭tcp连接* param upcb: UDP控制块* retval 无*/ void lwip_demo_connection_close(struct udp_pcb *upcb) {udp_disconnect(upcb);udp_remove(upcb); /* 断开UDP连接*/udp_demo_flag ~(1 5); /* 标记连接断开*/lcd_clear(BLACK); /* 清屏*/lcd_show_string(30, 30, 200, 16, 16, STM32, g_point_color);lcd_show_string(30, 50, 200, 16, 16, UDP Test, g_point_color);lcd_show_string(30, 70, 200, 16, 16, ATOM正点原子, g_point_color);lcd_show_string(30, 90, 200, 16, 16, KEY1:Connect, g_point_color);lcd_show_string(30, 190, 210, 16, 16, STATUS:Disconnected, g_point_color); } main 函数一开始完成外设的初始化如果开启DHCP 的话通过DHCP 获取IP 地址IP 地 址获取成功以后就调用udp_demo_test 函数进入UDP 实验。我们知道在lwip_demo 函数中有一个while()循环当从这个循环退出来以后就会进入main 函数的while()循环中在main 函数 的while()循环中当KEY1 按下并且UDP 连接已经断开就调用lwip_demo 函数重新开始UDP 实 验如下源码所示 key key_scan(0); if (key KEY1_PRES) {if ((lwip_send_flag 1 5)){printf(UDP连接已经建立,不能重复连接\r\n); /* 如果连接成功,不做任何处理*/}else{lwip_demo_test(); /* 当断开连接后,调用udp_demo_test()函数*/} } 下载验证 下载完代码后打开网络调试助手等待开发板的LCD 出现如图11.4.1 所示界面在这 个界面上我们通过按键KEY2 和KEY0 设置远端IP 地址也就是电脑的IP 地址设置好以后 按KEY_UP 确认确认完了以后LCD 就如图11.4.2 所示的数据接收界面。 图11.3.3.1 远端IP 地址设置 接下来设置电脑端的网络调试助手设置完成后点击网络调试助手的“连接”操作完后 的网络调试助手如下图所示 设置完网络调试助手后在发送填入要发送的数据这里输入要发送的数据ALIENTEK DATA然后点击发送这时我们开发板的LCD 上显示我们可以看到在LCD 上显示出了电 脑端发送过来的数据我们通过按下KEY0向电脑端发送数据“ALIENTEK DATA”图 11.4.4 所示表明网络调试助手接收到开发板发送的数据这里我们按了12 次KEY0因此在 网络调试助手上有12 行数据。 图11.3.3.4 UDP 测试 RAW 编程接口TCP 服务器实验 在本章中开发板做TCP 服务器网络调试助手做TCP 客户端实验中我们通过电脑端的 网络调试助手给开发板发送数据开发板接收并在LCD 上显示接收到的数据同时也可以通 过按键从开发板向网络调试助手发送数据。 RAW 编程接口TCP 简介 在上一章RAW 编程接口的TCP 客户端实验中我们已经讲解过了TCP 的基础知识这里 就不做讲解。 RAW 接口的TCP 实验 硬件设计 例程功能 本章实验的目标是PC 端和开发板通过TCP 协议连接起来开发板做TCP 服务器PC 端 的网络调试助手配置成客户端。网络调试助手连接到开发板服务器时网络调试助手可向开发 板发送数据并且在LCD 上显示我们也可以通过开发板上的按键发送数据给PC。 该实验的实验工程请参考《lwIP 例程4 lwIP_RAW_TCPServer 实验》。 软件设计 13.2.2.1 TCP 服务器配置步骤 创建TCP 控制块 调用函数tcp_new 创建TCP 控制块。绑定本地IP 地址和端口号 调用函数tcp_bind 绑定本地IP 地址和端口号。连接请求 调用函数tcp_accept 等待连接。接收数据 调用函数tcp_recved 接收数据。发送数据 调用函数tcp_write 发送数据。 13.2.2.2 程序流程图 本实验的程序流程图如下图所示 程序解析 上一章中我们简单的介绍了几个lwIP 中关于TCP 的函数本节中我们就用这几个函数编 写我们本章的例程本章实验的目标是PC 端和开发板通过TCP 协议连接起来开发板做 TCP 服务器PC 端的网络调试助手配置成客户端。网络调试助手连接到开发板服务器网络 调试助手向开发板发送数据并在LCD 上显示接收到的数据我们也可以通过开发板上的按键 发送数据给PC。本章实验中我们主要有两个文件lwip_demo.c 和lwip_demo.h。lwip_demo.h 文件很简单这里就不讲解我们重点讲解一下lwip_demo.c 这个文件在tcp_server_demo.c 文件中我们一共定义了9 个函数如下表所示。 lwip_tcp_server_accept 函数为控制块accpet 字段的回调函数当一个侦听和其他主机连接 上以后调用在这个函数中我们主要是为控制块的相应字段注册回调函数函数的代码如下。 /*** brief lwIP tcp_accept()的回调函数* param arg 传入的参数* param newpcbTCP控制块* param err 错误码* retval 返回ret_err*/ err_t lwip_tcp_server_accept(void *arg, struct tcp_pcb *newpcb, err_t err) {err_t ret_err;struct tcp_server_struct *es;LWIP_UNUSED_ARG(arg);LWIP_UNUSED_ARG(err);tcp_setprio(newpcb, TCP_PRIO_MIN); /* 设置新创建的pcb优先级*//* 分配内存*/es (struct tcp_server_struct *)mem_malloc(sizeof(struct tcp_server_struct));if (es ! NULL) /* 内存分配成功*/{es-state ES_TCPSERVER_ACCEPTED; /* 接收连接*/es-pcb newpcb;es-p NULL;tcp_arg(newpcb, es);tcp_recv(newpcb, lwip_tcp_server_recv); /* 初始化tcp_recv()的回调函数*/tcp_err(newpcb, lwip_tcp_server_error); /* 初始化tcp_err()回调函数*/tcp_poll(newpcb, lwip_tcp_server_poll, 1); /* 初始化tcp_poll回调函数*/tcp_sent(newpcb, lwip_tcp_server_sent); /* 初始化发送回调函数*/lwip_send_flag | 1 5; /* 标记有客户端连上了*/lwipdev.remoteip[0] newpcb-remote_ip.addr 0xff;lwipdev.remoteip[1] (newpcb-remote_ip.addr 8) 0xff;lwipdev.remoteip[2] (newpcb-remote_ip.addr 16) 0xff;lwipdev.remoteip[3] (newpcb-remote_ip.addr 24) 0xff;ret_err ERR_OK;}elseret_err ERR_MEM;return ret_err; } lwip_tcp_server_recv 函数和上一章TCP 客户端实验的lwip_tcp_client_recv 函数功能基本 差不多大家可以对照这两个函数的源码看一下这里就不做讲解了。 lwip_tcp_server_error 为当出现重大错误的时候的回调函数在TCP 客户端实验中我们没 有实现这个函数在本章实验中我们将这个函数的参数通过串口打印出来当然也可以根据自 己的实际情况来实现这个函数。 lwip_tcp_server_polllwip_tcp_server_sent 和lwip_tcp_server_senddata 这三个函数分别和上一章TCP 客户端实验中的lwip_tcp_client_polllwip_tcp_client_sent 和lwip_tcp_client_sendd ata 函数功能类似大家可以参考TCP 客户端实验中关于这三个函数的讲解。 lwip_tcp_server_connection_close 函数用来关闭TCP 连接我们通过调用tcp_close 函数来 关闭连接注意这里和TCP 客户端实验的不同然后就是注销掉控制块中的回调函数最后 释放内存清零lwip_send_flag 的bit5标记连接断开函数代码如下。 /*** brief 关闭tcp连接* param tpcb TCP控制块* param es LWIP回调函数使用的结构体* retval 无*/ void tcp_server_connection_close(struct tcp_pcb *tpcb, struct tcp_server_struct *es) {tcp_close(tpcb);tcp_arg(tpcb, NULL);tcp_sent(tpcb, NULL);tcp_recv(tpcb, NULL);tcp_err(tpcb, NULL);tcp_poll(tpcb, NULL, 0);if (es)mem_free(es);lwip_send_flag ~(1 5); /* 标记连接断开了*/lcd_show_string(30, 30, 200, 16, 16, STM32, g_point_color);lcd_show_string(30, 50, 200, 16, 16, TCPServer Test, g_point_color);lcd_show_string(30, 70, 200, 16, 16, ATOM正点原子, g_point_color);lcd_show_string(30, 90, 200, 16, 16, KEY1:Connect, g_point_color);lcd_show_string(30, 190, 210, 16, 16, STATUS:Disconnected, g_point_color); } 最后一个函数是lwip_tcp_server_remove_timewait这个函数用来强制删除处于TIME-WA TI 状态的控制块函数代码如下。 /*** brief 强制删除TCP Server主动断开时的time wait* param 无* retval 无*/ void lwip_tcp_server_remove_timewait(void) {struct tcp_pcb *pcb, *pcb2;uint8_t t 0;while (tcp_active_pcbs ! NULL t 200){lwip_periodic_handle(); /* 继续轮询*/t;delay_ms(10); /* 等待tcp_active_pcbs为空*/}pcb tcp_tw_pcbs;while (pcb ! NULL) /* 如果有等待状态的pcbs */{tcp_pcb_purge(pcb);tcp_tw_pcbs pcb-next;pcb2 pcb;pcb pcb-next;memp_free(MEMP_TCP_PCB, pcb2);} } 接下来是本实验最重要的函数lwip_demo同UDP 实验和TCP 客户端实验一样这个函 数一开始也是显示一些提示信息不过不同的是和本实验不用设置需要连接的远端主机的IP 地址因为服务器是等待其他主机来连接的。显示完提示信息以后就是本函数的重点了代码 如下。 /*** brief lwip_demo 测试* param 无* retval 无*/ void lwip_demo(void) {err_t err;struct tcp_pcb *tcppcbnew; /* 定义一个TCP服务器控制块*/struct tcp_pcb *tcppcbconn; /* 定义一个TCP服务器控制块*/char *tbuf;uint8_t key;uint8_t res 0;uint8_t t 0;uint8_t connflag 0; /* 连接标记*/lcd_clear(BLACK); /* 清屏*/g_point_color WHITE;lcd_show_string(30, 30, 200, 16, 16, STM32, g_point_color);lcd_show_string(30, 50, 200, 16, 16, TCP Server Test, g_point_color);lcd_show_string(30, 70, 200, 16, 16, ATOM正点原子, g_point_color);lcd_show_string(30, 90, 200, 16, 16, KEY0:Send data, g_point_color);lcd_show_string(30, 110, 200, 16, 16, KEY_UP:Quit, g_point_color);tbuf mymalloc(SRAMIN, 200); /* 申请内存*/if (tbuf NULL)return; /* 内存申请失败了,直接退出*/sprintf((char *)tbuf, Server IP:%d.%d.%d.%d, lwipdev.ip[0],lwipdev.ip[1],lwipdev.ip[2],lwipdev.ip[3]);lcd_show_string(30, 130, 210, 16, 16, tbuf, g_point_color);sprintf((char *)tbuf, Server Port:%d, LWIP_DEMO_PORT); /* 服务器端口号*/lcd_show_string(30, 150, 210, 16, 16, tbuf, g_point_color);tcppcbnew tcp_new(); /* 创建一个新的pcb */if (tcppcbnew) /* 创建成功*/{/* 将本地IP与指定的端口号绑定在一起,IP_ADDR_ANY为绑定本地所有的IP地址*/err tcp_bind(tcppcbnew, IP_ADDR_ANY, LWIP_DEMO_PORT);if (err ERR_OK) /* 绑定完成*/{tcppcbconn tcp_listen(tcppcbnew); /* 设置tcppcb进入监听状态*//* 初始化LWIP的tcp_accept的回调函数*/tcp_accept(tcppcbconn, tcp_server_accept);}elseres 1;}elseres 1;g_point_color BLUE;while (res 0){key key_scan(0);if (key KEY1_PRES)break;if (key KEY0_PRES) /* KEY0按下了,发送数据*/{lwip_tcp_server_usersent(tcppcbnew); /* 发送数据*/}if (lwip_send_flag 1 6) /* 是否收到数据*/{/* 清上一次数据*/lcd_fill(30, 210, lcddev.width - 1, lcddev.height - 1, BLACK);/* 显示接收到的数据*/lcd_show_string(30, 210, lcddev.width - 30, lcddev.height - 210, 16,(char *)lwip_demo_recvbuf, g_point_color);lwip_send_flag ~(1 6); /* 标记数据已经被处理了*/}if (lwip_send_flag 1 5) /* 是否连接上*/{if (connflag 0){/* 客户端IP */sprintf((char *)tbuf,ClientIP:%d.%d.%d.%d, lwipdev.remoteip[0],lwipdev.remoteip[1],lwipdev.remoteip[2],lwipdev.remoteip[3]);lcd_show_string(30, 170, 230, 16, 16, tbuf, g_point_color);g_point_color WHITE;connflag 1; /* 标记连接了*/}}else if (connflag){lcd_fill(30, 170, lcddev.width - 1, lcddev.height - 1, BLACK);connflag 0; /* 标记连接断开了*/}lwip_periodic_handle();delay_ms(2);t;if (t 200){t 0;LED0_TOGGLE();}}lwip_tcp_server_connection_close(tcppcbnew, 0); /* 关闭TCP Server连接*/lwip_tcp_server_connection_close(tcppcbconn, 0); /* 关闭TCP Server连接*/lwip_tcp_server_remove_timewait();memset(tcppcbnew, 0, sizeof(struct tcp_pcb));memset(tcppcbconn, 0, sizeof(struct tcp_pcb));myfree(SRAMIN, tbuf); } 在上面代码中主要完成一下几个功能。 (1) 通过调用tcp_new 函数创建一个tcp 控制块tcppcbnew这个控制块用来进行监听如 果未创建成功的话就令res 等于1。 (2) 当控制块tcppcbnew 创建成功以后就将其绑定到指定的IP 地址和端口好上绑定成功 以后将控制块设置为监听状态并且注册控制块accept 字段的回调函数如果绑定未成功的话 就让res 等于1。 (3) 当res 等于0 的话就进入while 循环在while 循环的处理过程基本和TCP 客户端的一 样。 (4) 当从while 循环退出来后我们就关闭TCP 连接这里我们要关闭两个tcppcbnew 和 tcppcbconn最后还要调用lwip_tcp_server_remove_timewait 函数将处于TIME—WAIT 状态的 pcb 控制块删除。 到这里lwip_demo.c 文件中的函数已经讲完。mian 函数就不必讲解了代码很简单。 下载验证 在代码编译成功以后我们下载代码到开发板中通过网线连接开发板到路由器上如果 没有路由器的话就连接到电脑端的RJ45 上电脑端还要进行设置设置过程很简单。开发板 上电等待出现13.2.3.1 所示画面打开网络调试助手按图13.2.3.2 所示设置好以后点击“连 接”按钮。 图13.2.3.1 开机LCD 显示画面 图13.2.3.2 网络调试助手设置 当网络调试助手连接上开发板以后开发板LCD 上显示如图13.2.3.3 所示表明网络调 试助手已经连接上服务器。 图13.2.3.3 连接上以后的LCD 显示 我们通过网络调试助手向开发板发送www.openedv.com此时开发板LCD 上显示接收 到的数据如图13.2.3.4 所示按下开发板上的KEY0 键向网络调试助手发送数据。 图13.2.3.4 LCD 显示接收到的数据 RAW 编程接口Web Server 实验 本章采用RAW 编程API 接口在开发板上实现一个WebServer。在本章中我们通过移植并 修改ST 官方的一个WebServer 的例程来展示。在浏览器中输入开发板的IP 地址来访问开发板 这时开发板会返回一个网页在这个网页上我们可以控制开发板的LED 灯和蜂鸣器并查看 ADC 和内部温度等外设的数据。 Web Server 文件以及相关技术简介 (1) 实验相关文件简介 本章我们在ST 官方Web Server 例程的基础上完成本章实验该参考实验是在 STM32Cube_FW_F4_V1.27.0\Projects\STM324x9I_EVAL\Applications\LwIP\LwIP_HTTP_Server _Raw 路径下获取这里我们打开《lwIP 例程4 lwIP_RAW_Webserver 实验》实验该实验用 到Middlewares\lwip\src\apps\http 文件夹下的文件这些文件如下图所示。 上图中altcp_proxyconnect.c 和http_client.c 文件并没有在本章实验中用到下面我们来讲 解一下本章用到的文件如下表所示 表14.1.1 Web Server 各文件说明 (2) 添加分组 在工程中添加Middlewares/lwip/lwip/src/apps 分组该分组添加上图14.1.1 的fs.c 以及 httpd.c 文件如下图所示 图14.1.2 Middlewares/lwip/lwip/src/apps 分组添加的文件 (3) lwipopts.h 文件添加配置项 /** 支持CGI */ #define LWIP_HTTPD_CGI 1 /** 支持SSI */ #define LWIP_HTTPD_SSI 1 /* 设置为1将包含fsdata_custom.c而不是fsdata.c */ #define HTTPD_USE_CUSTOM_FSDATA 0 上述源码的第一和第二配置项表示支持CGI 和SSI 技术这些技术我们稍后讲解第三 个配置项主要表示上图14.1.2 中fs.c 文件包含的文件路径这里我们使用fsdata.c 文件路径。 注意这个fadata.c 文件包含了网页数组这些网页数组的生成方法我们稍后讲解 (4) 添加工程路径 点击魔法棒并进入C/C配置项页面我们在这个页面下添加“…\Middlewares\lwip\src\i nclude\lwip\apps”头文件路径。 (5) 网页数组制作 在lwip\src\apps\http\路径下包含了makefsdata 文件夹该文件夹中的文件可以把网页生成 网页数组这里我们不使用这个文件夹我们使用正点原子之前的makefsdata 文件夹该文件 夹包含了makefsdata.exe我们使用这个软件自动生成即可。 首先我们复制正点原子之前的lwip 实验的下的makefsdata 文件夹到桌面上makefsdata 文 件夹保存着网页数组生成器以及fs 文件夹网页数组生成如下图14.1.3 所示而fs 文件夹保 存着网页源文件如图14.1.4 所示 图14.1.3 makefsdata 文件夹的内容 图14.1.4 本实验网页源文件 图14.1.4 中的是网页源文件显然这个文件不能直接放到STM32 里面我们要做一个转换使其可以放到STM32 里面。这里我们通过makefsdata.exe 这个工具将原始网页文件转换 成.c 格式的网页数组这样就可以添加到我们的工程中了下面我们讲解makefsdata 工具的使 用。 (6) makefsdata 工具的使用 makefsdata 工具是用来将我们编辑网页文件等信息转换成二进制数的一个工具。接下来我 们讲解一下这个工具的使用方法。 ①将fs 文件夹和makefsdada.exe 工具放到同一文件夹下,此处为makefsdata 文件夹打开 makefsdata 文件夹,如图14.1.5 所示。其中图14.1.3 中的echotool.exe 和Tftpd32-3.51-setup.exe 为其他工具这里没有使用到cmd.reg 稍后我们会讲到。 图14.1.5 makefsdata 文件内容 ②快捷键“winr”并输入cmd 进入命令行我们复制上图的makafsdata 文件夹路径并在 命令行上进入该路径如图14.1.6 所示 图14.1.6 在命令行下进入makafsdata 文件夹 如果我们不想使用命令行的方式生成网页数组请使用我们提供的cmd.reg 文件导入注册 表注册,双击打开cmd.reg然后一路确定下去就可以了。 ③在上图14.1.6 命令行中输入“makefsdata –i”命令并按回车生成网页数组如下图所示。 ④打开makefsdata 文件夹打开后如图14.1.8 所示我们发现在makefsdata 文件夹下多 了一个fsdata.c 的C 文件这个fsdata.c 文件就是我们转换后的二进制网页文件到此 makefsdata 工具的用法介绍完毕 图14.1.8 生成的fsdata.c 文件 (7) 把上图的fsdata.c 文件替换Middlewares\lwip\src\apps\http 路径下的fsdata.c 文件。注 意请把file__404_html 网页数组和file__index_html 网页数组添加到fsdata.c 文件中不然 本实验无法运行。 (8) 添加httpd_cgi_ssi.c 文件 该httpd_cgi_ssi.c 文件是正点原子根据ST 官方的一个WebServer 的例程改编的这里例程 路径为STM32Cube_FW_F4_V1.27.0\Projects\STM324xG_EVAL\Applications\LwIP\LwIP_HTT P_Server_Raw\Src 路径下的httpd_cgi_ssi.c注意ST 官方的WebServer 例程使用的是fsdata_c ustom.c 文件所以它把HTTPD_USE_CUSTOM_FSDATA 配置项设置为1。我们把正点原子改 编的httpd_cgi_ssi.c 文件添加到Middlewares/lwip/lwip_app 分组当中如图14.1.10 所示 图14.1.10 添加httpd_cgi_ssi.c 文件 (9) 在lwip_demo 函数添加httpd 初始化源码 #include httpd.h #include httpd_cgi_ssi.h /*** brief lwip_demo 测试* param 无* retval 无*/ void lwip_demo(void) {/* Httpd Init */httpd_init();/* 配置SSI处理程序*/httpd_ssi_init();/* 配置CGI处理器*/httpd_cgi_init(); } 至此我们已经完成了WebServer 例程改编下面我们来讲解一下CGI 和SSI 技术。 (10) CGI 技术简介 公共网关接口CGI(Common Gateway Interface) 是WWW 技术中最重要的技术之一有着 不可替代的重要地位。CGI 是外部应用程序与Web 服务器之间的接口标准是在CGI 程序和 Web 服务器之间传递信息的规程。CGI 规范允许Web 服务器执行外部程序并将它们的输出 发送给Web 浏览器CGI 在物理上是一段程序运行在服务器上提供同客户端HTML 页面 的接口。 绝大多数的CGI 程序被用来解释处理来自表单的输入信息并在服务器产生相应的处理 或将相应的信息反馈给浏览器CGI 程序使网页具有交互功能。在我们本章实验中我们通过浏 览器控制开发板上的LED 和蜂鸣器就是使用的CGI 技术。 (11) SSI 技术简介 服务器端嵌入Server Side Include是一种类似于ASP 的基于服务器的网页制作技术。 大多数的WEB 服务器等均支持SSI 命令。将内容发送到浏览器之前可以使用“服务器端包 含(SSI”指令将文本、图形或应用程序信息包含到网页中。例如可以使用SSI 包含时间/日 期戳、版权声明或供客户填写并返回的表单。对于在多个文件中重复出现的文本或图形使用 包含文件是一种简便的方法。将内容存入一个包含文件中即可而不必将内容输入所有文件。 通过一个非常简单的语句即可调用包含文件此语句指示Web 服务器将内容插入适当网页。 而且使用包含文件时对内容的所有更改只需在一个地方就能完成。因为包含SSI 指令的文 件要求特殊处理所以必须为所有SSI 文件赋予SSI 文件扩展名。默认扩展名是.stm、.shtm 和.shtml。 SSI 是为WEB 服务器提供的一套命令这些命令只要直接嵌入到HTML 文档的注释内容 之中即可。如 就是一条SSI 指令其作用是将info.htm的内容 拷贝到当前的页面中当访问者来浏览时会看到其它HTML 文档一样显示info.htm 其中的 内容。其它的SSI 指令使用形式基本同刚才的举例差不多可见SSI 使用只是插入一点代码而 已使用形式非常简单。 是HTML 语法中表示注释当WEB 服务器不支持SSI 时会 忽略这些信息。 在本实验中我们可以通过网页查看开发板的ADC内部温度传感器和RTC 的值就是通过 SSI 来实现的。 Web Server 实验 硬件设计 例程功能 本实验通过网页控制开发板上的LED 和BEEP 外设当然可以接收开发板的RTC 以及 ADC 数据并在网页上显示出来。 该实验的实验工程请参考《lwIP 例程5 lwIP_RAW_WebServer 实验》。 软件设计 14.2.2.1 程序流程图 本实验的程序流程图如下图所示 图14.2.2.1.1 Webserver 实验流程图 14.2.2.2 程序解析 我们打开《lwIP 例程4 lwIP_RAW_Webserver 实验》的工程如图14.2.2.2.1 所示其中 fs.c 文件管理生成的网页数组文件这个文件由lwIP 提供。httpd.c 文件是本章实验的重点这个 文件将开发板配置为Web Server这个文件也由lwIP 官方提供的阅读这个文件需要有网页 相关的知识这里对这个文件不做讲解。 我们在浏览器中输入网址服务器就会返回给我们相应的网页然后浏览器解析并呈现给 我们。同样的当我们通过浏览器访问开发板的时候开发板这时是作为服务器的服务器针 对不同的URL 在fsdata.c 文件中找出相应的网页并且返回给浏览器在fsdata.c 文件中查找 网页的过程就需要fs.c 里面的函数。接收浏览器发送的数据并且将网页返回给浏览器的过程都 是由httpd.c 文件里面的函数来完成的。 fs.c 和httpd.h 文件本章不做讲解感兴趣的朋友可以去看一下本章中我们主要讲解的是 httpd_cgi_ssi.c 这个文件这个文件中讲解了如何使用CGI 和SSI 技术完成浏览器与服务器的 交互。 图14.2.2.2.1 Web Server 工程文件 (1) CGI 实现 我们通过浏览器控制开发板上的LED 灯和蜂鸣器就是使用的CGI 技术我们在开发板上 对浏览器发送过来的URL 进行分析然后根据不同的URL 调用不同的程序就可以了下图 14.2.2.2.2 中我们控制LED1 灯亮注意图中的URL。 图14.2.2.2.2 打开LED1 上图中的URL 为http://192.168.1.136/leds.cgi?LED1LED1ONbutton2SEND。我们就 是分析这一串字符串来做出相应的处理其中leds.cgi 表示为控制LED 灯的CGI后面的 “LED1”为变量LEDION 为变量“LED1”的值。我们根据字符串“leds.cgi”调用处理 LED 灯的程序然后根据后面的变量和变量值来决定是打开还是关闭LED 灯。在 httpd_cgi_ssi.c 中我们定义了一个数组ppcURLS数组如下在这个数组中leds.cgi 对应的 LEDS_CGI_Handler 处理程序beep.cgi 对应的是BEEP_CGI_Handler 处理程序。 static const tCGI ppcURLs[] /* cgi程序*/ {{/leds.cgi, LEDS_CGI_Handler},{/beep.cgi, BEEP_CGI_Handler}, }; LEDS_CGI_Handler 是一个函数代码如下从下面代码中可以看出我们是根据变量 “LED1”的值来做相应的处理当为LED1 ON 的时候就打开LED1当为LED1 OFF 的时候 就关闭LED1。 那么蜂鸣器BEEP 的处理过程也一样的这里就不做讲解。最后我们还要还要初始化CGI 句 柄初始化函数为httpd_cgi_init这个函数很简单。 /* CGI LED控制句柄*/ const char *LEDS_CGI_Handler(int iIndex, int iNumParams, char *pcParam[],char *pcValue[]) {uint8_t i 0; /* 注意根据自己的GET的参数的多少来选择i值范围*/iIndex FindCGIParameter(LED1, pcParam, iNumParams);/* 只有一个CGI句柄iIndex0 */if (iIndex ! -1){LED1(1);/* 检查CGI参数: example GET /leds.cgi?led2led4 */for (i 0; i iNumParams; i){if (strcmp(pcParam[i], LED1) 0) /* 检查参数led */{if (strcmp(pcValue[i], LED1ON) 0) /* 改变LED1状态*/{/* 打开LED1 */HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, GPIO_PIN_RESET);}else if (strcmp(pcValue[i], LED1OFF) 0){/* 关闭LED1 */HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, GPIO_PIN_SET);}}}}if (READ_LED1 0 READ_BEEP 0){return /STM32F407LED_ON_BEEP_OFF.shtml; /* LED1开,BEEP关*/}else if (READ_LED1 0 READ_BEEP 1){return /STM32F407LED_ON_BEEP_ON.shtml; /* LED1开,BEEP开*/}else if (READ_LED1 1 READ_BEEP 1){return /STM32F407LED_OFF_BEEP_ON.shtml; /* LED1关,BEEP开*/}else{return /STM32F407LED_OFF_BEEP_OFF.shtml; /* LED1关,BEEP关*/} } (2) SSI 实现 我们通过网页查看开发板上的ADC 值内部温度传感器和RTC 时间的时候就是用的SSI 每隔1s 刷新一次网页然后通过SSI 技术将这些值嵌入到网页中这样我们就看到时钟在动 态的跟新如下图所示。 SSIHandler 函数为SSI 的句柄函数函数代码如下在这个函数中我们根据参数iIndex 调 用不同的函数来完成向网页中添加数据这几个函数比较简单大家自行查阅一下。最后我们 还要还要初始化SSI 句柄初始化函数为httpd_ssi_init这个函数很简单。 /* SSI的Handler句柄*/ static u16_t SSIHandler(int iIndex, char *pcInsert, int iInsertLen) {switch (iIndex){case 0:ADC_Handler(pcInsert);break;case 1:Temperate_Handler(pcInsert);break;case 2:RTCTime_Handler(pcInsert);break;case 3:RTCdate_Handler(pcInsert);break;}return strlen(pcInsert); } 下载验证 在代码编译成功以后我们下载代码到开发板中通过网线连接开发板到路由器上如果 没有路由器的话就连接到电脑端的RJ45 上电脑端还要进行设置设置过程和UDP 实验一样。 下载完成后等待开发板LCD 出现如图14.2.3.1 所示然后我们在浏览器里面输入开发板的IP 地址我的开发板IP 地址为192.168.1.136大家根据自己的实际情况输入就行了按回车 服务器将网页返回给浏览器并显示出来如图14.2.3.2 所示。 图14.2.3.1 LCD 显示界面 图14.2.3.2 WEB Server 主页 最后大家可以测试一下其他的功能比如控制开发板的LED1 灯蜂鸣器等可以查看 ADC 的值和内部温度传感器查看RTC 时间值。 NETCONN 编程接口简介 前几章的实验我们都没有使用操作系统因此它们采用的是RAW 编程接口RAW 编程 接口使得程序效率高但是需要对lwIP 有深入的了解而且不适合大数据量等场合。本章我 们就讲解一下NETCONN 编程接口使用NETCONN API 时需要有操作系统的支持我们使 用的是FreeRTOS 操作系统。 声明本章内容参考自《嵌入式网络那些事LWIP 协议深度剖析与实战演练》作者朱升 林由于该书是使用lwIP1.3.2 版本所以它和本教程的lwIP2.1.2 源码存在许多小差异。 netbuf 数据缓冲区 在前面笔者讲过了描述数据包的pbuf 结构这里我们就不做详细讲解了本章我们来讲 解一下另一个数据包的netbuf 结构netbuf 是NETCONN 编程API 接口使用的描述数据包的 结构我们可以使用netbuf 来管理发送数据、接收数据的缓冲区。有关netbuf 的详细描述在 netbuf.c 和netbuf.h 这两个文件中netbuf 是一个结构体它在netbuf.h 中定义了这个结构体 代码如下 /* 网络缓冲区包含数据和寻址信息*/ struct netbuf {/* p 字段的指针指向pbuf 链表*/struct pbuf *p, *ptr;ip_addr_t addr; /* 记录了数据发送方的IP 地址*/u16_t port; /* 记录了数据发送方的端口号*/ }; 从上述源码可知其中p 和ptr 都指向pbuf 链表不同的是p 一直指向pbuf 链表的第一 个pbuf 结构而ptr 可能指向链表中其他的位置lwIP 提供的netbuf_next 和netbuf_first 函数 都是操作ptr 字段的。addr 和port 字段用来记录数据发送方的IP 地址和端口号lwIP 提供的 netbuf_fromaddr 和netbuf_fromport 函数用于返回addr 和port 这两个字段。从上述的描述我 们可以知道netbuf 是用来管理发送数据和接收数据的缓冲区这些收发数据都存储在pbuf 数 据缓冲区当中所以netbuf 和pbuf 肯定有着某种联系下面我们根据上述的netbuf 结构体来 介绍一下netbuf 和pbuf 之间的关系如下图所示。 从上图可知netbuf 结构体中的p 指针永远指向pbuf 链表的第一个而ptr 可能指向链表 中其他的位置。 不管是TCP 连接还是UDP 连接接收到数据包后会将数据封装在一个netbuf 中然后将 这个netbuf 交给应用程序去处理。在数据发送时根据不同的连接有不同的处理对于TCP 连接用户只需要提供待发送数据的起始地址和长度内核会根据实际情况将数据封装在合适 大小的数据包中并放入发送队列中对于UDP 来说用户需要自行将数据封装在netbuf 结 构中当发送函数被调用的时候内核直接将数据包中的数据发送出去。 在netbuf.c 中提供了几个操作netbuf 的函数如下表所示。 注意用户使用NETCONN 编程API 接口时必须在lwipopts.h 把LWIP_NETCONN 配置 项设置为1 启动NETCONN 编程API 接口。 netconn 连接结构 我们前面在使用RAW 编程接口的时候对于UDP 和TCP 连接使用的是两种不同的编程 函数udp_xxx 和tcp_xxx。NETCONN 对于这两种连接提供了统一的编程接口用于使用同 一的连接结构和编程函数在api.h 中定了netcon 结构体代码如下。 /* netconn描述符*/ struct netconn {/* 连接类型TCP UDP或者RAW */enum netconn_type type;/* 当前连接状态*/enum netconn_state state;/* 内核中与连接相关的控制块指针*/union{struct ip_pcb *ip; /* IP控制块*/struct tcp_pcb *tcp; /* TCP控制块*/struct udp_pcb *udp; /* UDP控制块*/struct raw_pcb *raw; /* RAW控制块*/} pcb;/* 这个netconn 最后一个异步未报告的错误*/err_t pending_err; #if !LWIP_NETCONN_SEM_PER_THREAD/* 用于两部分API同步的信号量*/sys_sem_t op_completed; #endif/* 接收数据的邮箱*/sys_mbox_t recvmbox; #if LWIP_TCP/* 用于TCP服务器端连接请求的缓冲队列*/sys_mbox_t acceptmbox; #endif /* LWIP_TCP */ /* Socket描述符用于Socket API */ #if LWIP_SOCKETint Socket; #endif /* LWIP_SOCKET */ #if LWIP_SO_RCVTIMEO/* 接收数据时的超时时间*/u32_t recv_timeout; #endif /* LWIP_SO_RCVTIMEO *//* 标识符*/u8_t flags; #if LWIP_TCP/* TCP:当传递到netconn_write的数据不适合发送缓冲区时这将临时存储消息。也用于连接和关闭。*/struct api_msg *current_msg; #endif /* LWIP_TCP *//* 连接相关回调函数实现Socket API时使用*/netconn_callback callback; }; 在api.h 文件中还定义了连接状态和连接类型这两个都是枚举类型。 /* 枚举类型用于描述连接类型*/ enum netconn_type {NETCONN_INVALID 0, /* 无效类型*/NETCONN_TCP 0x10, /* TCP */NETCONN_UDP 0x20, /* UDP */NETCONN_UDPLITE 0x21, /* UDPLite */NETCONN_UDPNOCHKSUM 0x22, /* 无校验UDP */NETCONN_RAW 0x40 /* 原始链接*/ }; /* 枚举类型用于描述连接状态主要用于TCP连接中*/ enum netconn_state {NETCONN_NONE, /* 不处于任何状态*/NETCONN_WRITE, /* 正在发送数据*/NETCONN_LISTEN, /* 侦听状态*/NETCONN_CONNECT, /* 连接状态*/NETCONN_CLOSE /* 关闭状态*/ }; 下面我们来结合一下netconn 的api 函数来讲解这个结构体的作用。 netconn 编程API 函数 本节我们就讲解一下NETCONN 编程的API 函数这些函数在api_lib.c 文件中实现的,其 中这个文件包含了很多netconn 接口的函数它们大部分是lwIP 内部调用的少部分是给用户 使用的用户能使用的函数如下表所示。 netconn_new 函数是函数netconn_new_with_proto_and_callback 的宏定义此函数用来为新 连接申请一个netconn 空间参数为新连接的类型连接类型在上一节已经讲过了常用的值 是NETCONN_UDP 和NETCONN_TCP分别代表UDP 连接和TCP 连接。该函数如下所示 #define netconn_new(t) netconn_new_with_proto_and_callback(t, 0, NULL) struct netconn * netconn_new_with_proto_and_callback(enum netconn_type t, u8_t proto,netconn_callback callback) {struct netconn *conn;API_MSG_VAR_DECLARE(msg);/* 第一步构建api_msg结构体*/API_MSG_VAR_ALLOC_RETURN_NULL(msg);/* 申请内存*/conn netconn_alloc(t, callback);if (conn ! NULL){err_t err;/* 连接协议*/API_MSG_VAR_REF(msg).msg.n.proto proto;/* 连接的信息*/API_MSG_VAR_REF(msg).conn conn;/* 构建API消息*/err netconn_apimsg(lwip_netconn_do_newconn, API_MSG_VAR_REF(msg));if (err ! ERR_OK) /* 构建失败*/{/* 释放信号量*/sys_sem_free(conn-op_completed);/* 释放邮箱*/sys_mbox_free(conn-recvmbox);/* 释放内存*/memp_free(MEMP_NETCONN, conn);API_MSG_VAR_FREE(msg);return NULL;}}API_MSG_VAR_FREE(msg);return conn; } 上述源码可知系统对conn 申请内存如果内存申请成功则系统构建API 消息。所谓 API 消息其实就是两个API 部分的交互的消息它是由用户的调用API 函数为起点使用IPC 通信机制告诉内核需要执行那个部分的API 函数API 消息的知识点笔者已经在第七章 7.4 小节讲解了下面笔者使用一个示意图来描述netconn_new 函数交互流程如下图所示 图15.3.1 用户的应用线程与内核交互示意图 相信大家对上图很熟悉吧没错这个图我们在第七章7.4.2 小节讲解过这些用户函数 就是以这个形式调用的。 netconn_delete 函数是用来删除一个netconn 连接结构如果函数调用时双方仍然处于连接 状态则相应连接将被关闭。其中对于UDP 连接它的连接会立即被关闭UDP 控制块被删 除对于TCP 连接该函数执行主动关闭内核完成剩余的断开握手过程该函数如下所示 err_t netconn_delete(struct netconn *conn) {err_t err;if (conn NULL){return ERR_OK;}/* 构建API消息*/err netconn_prepare_delete(conn);if (err ERR_OK){netconn_free(conn);}return err; } 此函数就是调用netconn_prepare_delete 函数构建API 消息API 构建流程请参考图15.3.1 的构建流程。 netconn_getaddr 函数是用来获取一个netconn 连接结构的源IP 地址和源端口号或者目的IP 地址和目的端口号IP 地址保存在addr 当中而端口信息保存在port 当中参数local 表示是 获取源地址还是目的地址当local 为1 时表示本地地址此函数原型如下。 err_t netconn_getaddr(struct netconn*conn,ip_addr_t*addr,u16_t*port,u8_t local);netconn_bind 函数将一个连接结构与本地IP 地址addr 和端口号port 进行绑定服务器端 程序必须执行这一步服务器必须与指定的端口号绑定才能结接受客户端的连接请求该函数原型如下。 err_t netconn_bind(struct netconn *conn, const ip_addr_t *addr, u16_t port); netconn_connect 函数的功能是连接服务器它将指定的连接结构与目的IP 地址addr 和目 的端口号port 进行绑定当作为TCP 客户端程序时调用此函数会产生握手过程该函数原 型如下。 err_t netconn_connect(struct netconn *conn, const ip_addr_t *addr, u16_t port); netconn_disconnect 函数只能使用在UDP 连接中功能是断开与服务器的连接。对于UDP 连接来说就是将UDP 控制块中的remote_ip 和remote_port 字段值清零函数原型如下。 err_t netconn_disconnect (struct netconn *conn); netconn_listen 函数只有在TCP 服务器程序中使用将一个连接结构netconn 设置为侦听状 态既将TCP 控制块的状态设置为LISTEN 状态该函数原型如下 #define netconn_listen(conn) netconn_listen_with_backlog(conn, TCP_DEFAULT_LISTEN_BACKLOG) netconn_accept 函数也只用于TCP 服务器程序服务器调用此函数可以从acceptmbox 邮箱 中获取一个新建立的连接若邮箱为空则函数会一直阻塞直到新连接的到来。服务器端调 用此函数前必须先调用netconn_listen 函数将连接设置为侦听状态函数原型如下。 err_t netconn_accept(struct netconn *conn, struct netconn **new_conn); netconn_recv 函数是从连接的recvmbox 邮箱中接收数据包可用于TCP 连接也可用于 UDP 连接函数会一直阻塞直到从邮箱中获得数据消息数据被封装在netbuf 中。如果从 邮箱中接收到一条空消息表示对方已经关闭当前的连接应用程序也应该关闭这个无效的连 接函数原型如下。 err_t netconn_recv(struct netconn *conn, struct netbuf **new_buf); netconn_send 函数用于在UDP 连接上发送数据参数conn 指出了要操作的连接参数 buf 为要发送的数据数据被封装在netbuf 中。如果IP 层分片功能未使能则netbuf 中的数据 不能太长不能超过MTU 的值最好不要超过1000 字节。如果IP 层分片功能使能的情况下 就可以忽略此细节函数原型如下。 err_t netconn_send(struct netconn *conn, struct netbuf *buf); netconn_write 函数用于在稳定的TCP 连接上发送数据参数dataptr 和size 分别指出了待 发送数据的起始地址和长度函数并不要求用户将数据封装在netbuf 中对于数据长度也没 有限制内核会直接处理这些数据将他们封装在pbuf 中并挂接到TCP 的发送队列中。 netconn_close 函数用来关闭一个TCP 连接该函数会产生一个FIN 握手包的发送成功 后函数便返回而后剩余的断开握手操作由内核自动完成用户程序不用关心该函数只是断 开一个连接但不会删除连接结构netconn用户需要调用netconn_delete 函数来删除连接结构 否则会造成内存泄漏函数原型如下。 err_t netconn_close(struct netconn *conn); NETCONN 编程接口UDP 实验 本章我们开始学习NETCONN API 函数的使用本章实验中我们通过电脑端的网络调 试助手给开发板发送数据开发板接收数据并通过串口将接收到的数据发送到串口调试助手上 也可以通过按键从开发板向网络调试助手发送数据。 NETCONN 实现UDP 上一章节我们已经知道lwIP 的NETCONN 编程接口API 的使用方法本章实验就是调 用这些API 函数来实现UDP 连接实验。用户使用NETCONN 编程接口实现UDP 连接分以下 几个步骤 ①调用函数netconn_new 创建UDP 控制块。 ②定义时间超时函数。 ③调用函数netconn_bind 绑定本地IP 和端口。 ④调用函数netconn_connect 建立连接。 关于UDP 的基础知识我们在第十一章时候已经讲过了这里就不重复讲解。 NETCONN 接口的UDP 实验 硬件设计 例程功能 本实验使用NETCONN 编程接口实现UDP 连接我们可通过按下KEY0 按键发送数据至 网络调试助手还可以接收网络调试助手发送的数据并在LCD 显示屏上显示。 该实验的实验工程请参考《lwIP 例程7 lwIP_NETCONN_UDP 实验》。 软件设计 16.2.2.1 netconn 的UDP 连接步骤 创建UDP 控制块 调用函数netconn_new 创建NETCONN 的UDP 控制块。连接本地IP 地址和端口号 调用函数netconn_bind 绑定本地IP 地址和本地端口号注意IP 地址必须是有效的。绑定远程IP 地址与端口号 调用函数netconn_connect 绑定远程IP 地址和远程端口号。接收数据 netconn_recv 接收数据。发送数据 调用函数netconn_send 发送数据。 程序流程图 本实验的程序流程图如下图所示 图16.2.2.2.1 NETCONN 编程UDP 实验流程图 16.2.2.3 程序解析 在本章实验中我们最主要关注两个文件它们分别为lwip_demo.c 和lwip_demo.h 文件 lwip_demo.h 文件主要定义了端口号、数据标识位以及声明lwip_demo 函数所以这些不需要 我们去讲解这里我们主要看lwip_demo.c 文件的函数。在16.1 小节中笔者已经列出实现 UDP 实验的步骤在此基础上调用NETCONN 接口配置UDP 连接。 void lwip_demo(void) {err_t err;static struct netconn *udpconn;static struct netbuf *recvbuf;static struct netbuf *sentbuf;ip_addr_t destipaddr;uint32_t data_len 0;struct pbuf *q;/* 第一步创建udp控制块*/udpconn netconn_new(NETCONN_UDP);/* 定义接收超时时间*/udpconn-recv_timeout 10;if (udpconn ! NULL) /* 判断创建控制块释放成功*/{/* 第二步绑定控制块、本地IP和端口*/err netconn_bind(udpconn, IP_ADDR_ANY, UDP_DEMO_PORT);IP4_ADDR(destipaddr, DEST_IP_ADDR0,DEST_IP_ADDR1,DEST_IP_ADDR2,DEST_IP_ADDR3); /*构造目的IP地址*//* 第三步连接或者建立对话框*/netconn_connect(udpconn, destipaddr, LWIP_DEMO_PORT);if (err ERR_OK) /* 绑定完成*/{while (1){/* 第四步如果指定的按键按下时会发送信息*/if ((lwip_send_flag LWIP_SEND_DATA) LWIP_SEND_DATA){sentbuf netbuf_new();netbuf_alloc(sentbuf, strlen((char *)udp_demo_sendbuf));memcpy(sentbuf-p-payload, (void *)udp_demo_sendbuf, |strlen((char *)udp_demo_sendbuf));err netconn_send(udpconn, sentbuf);if (err ! ERR_OK){printf(发送失败\r\n);netbuf_delete(sentbuf); /* 删除buf */}lwip_send_flag ~LWIP_SEND_DATA; /* 清除数据发送标志*/netbuf_delete(sentbuf); /* 删除buf */}/* 第五步接收数据*/netconn_recv(udpconn, recvbuf);if (recvbuf ! NULL) /* 接收到数据*/{/*数据接收缓冲区清零*/memset(lwip_demo_recvbuf, 0, LWIP_DEMO_RX_BUFSIZE);/*遍历完整个pbuf链表*/for (q recvbuf-p; q ! NULL; q q-next){if (q-len (LWIP_DEMO_RX_BUFSIZE - data_len))memcpy(lwip_demo_recvbuf data_len, q-payload,(LWIP_DEMO_RX_BUFSIZE - data_len)); /* 拷贝数据*/elsememcpy(lwip_demo_recvbuf data_len,q-payload, q-len);data_len q-len;if (data_len LWIP_DEMO_RX_BUFSIZE)break;}data_len 0; /* 复制完成后data_len要清零*/printf(%s\r\n, lwip_demo_recvbuf); /* 打印接收到的数据*/netbuf_delete(recvbuf); /* 删除buf */}elsevTaskDelay(5); /* 延时5ms */}}elseprintf(UDP绑定失败\r\n);}elseprintf(UDP连接创建失败\r\n); } 可以看出笔者调用NETCONN 接口配置UDP 连接配置完成之后调用netconn_send 发 送数据到服务器当中当然我们也可以调用netconn_recv 函数接收服务器的数据。 下载验证 代码编译完成后下载到开发板中初始化完成之后我们来看一下LCD 显示的内容如下 图所示 图16.2.3.1 LCD 显示 我们在来看一下串口调试助手如图16.2.3.2 所示在串口调试助手上也输出了我们开发板 的IP 地址子网掩码、默认网关等信息。 图16.2.3.2 串口调试助手 我们通过网络调试助手发送数据到开发板当中结果如图16.2.3.3 所示当然我们可以通 过开发板上的KEY0 发送数据到网络调式助手当中如图16.2.3.4 所示 图16.2.3.3 LCD 显示 图16.2.3.4 网络调试助手接收数据 NETCONN 编程接口TCP 服务器实验 本章实验中开发板做TCP 服务器网络调试助手做客户端网络调试助手连接TCP 服务 器(开发板)。连接成功后网络调试助手可以给开发板发送数据开发板接收数据并通过串口将 接收到的数据发送到串口调试助手上也可以通过按键从开发板向网络调试助手发送数据。本 章分为如下几个部分 19.1 NETCONN 实现TCP 服务器 19.2 NETCONN 接口的TCPServer 实验 NETCONN 实现TCP 服务器步骤 NETCONN 实现TCP 服务器有以下几步 ①调用函数netconn_new 创建TCP 控制块。 ②调用函数netconn_bind 绑定TCP 控制块、本地IP 地址和端口号。 ③调用函数netconn_listen 进入监听模式。 ④设置接收超时时间conn-recv_timeout。 ⑤调用函数netconn_accept 接收连接请求。 ⑥调用函数netconn_getaddr 获取远端IP 地址和端口号。 ⑦调用函数netconn_write 和netconn_recv 收发数据。 至于TCP 协议的知识请大家参看第十二章的内容。 NETCONN 接口的TCPServer 实验 硬件设计 例程功能 本实验使用NETCONN 编程接口实现TCPServer 连接我们可通过按下KEY0 按键发送 数据至网络调试助手还可以接收网络调试助手发送的数据并在LCD 显示屏上显示。 该实验的实验工程请参考《lwIP 例程9 lwIP_NETCONN_TCPServer 实验》。 软件设计 18.2.2.1 netconn 的TCPServer 连接步骤 创建TCP 控制块 调用函数netconn_new ()创建TCP 控制块。绑定TCP 控制块、本地IP 地址和端口号 调用函数netconn_bind()绑定本地IP 地址和端口号。配置监听模式 调用函数netconn_listen ()进入监听模式。接收连接请求 调用函数netconn_accept()接收连接请求接收数据 调用函数netconn_recv ()接收数据。发送数据 调用函数netconn_write ()发送数据。 18.2.2.2 程序流程图 本实验的程序流程图如下图所示 程序解析 打开我们的例程找到lwip_demo.c 和lwip_demo.h 两个文件这两个文件就是我本章实 验的源码在lwip_demo.c 中我们实现了一个函数lwip_demo同上一章一样都有操作系统 的支持下如下源码所示 void lwip_demo(void) {uint32_t data_len 0;struct pbuf *q;err_t err, recv_err;uint8_t remot_addr[4];struct netconn *conn, *newconn;static ip_addr_t ipaddr;static u16_t port;/* 第一步创建一个TCP控制块*/conn netconn_new(NETCONN_TCP); /* 创建一个TCP链接*//* 第二步绑定TCP控制块、本地IP地址和端口号*/netconn_bind(conn, IP_ADDR_ANY, LWIP_DEMO_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);while (1){/*有数据要发送*/if ((lwip_send_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){}lwip_send_flag ~LWIP_SEND_DATA;}/*接收到数据*/if ((recv_err netconn_recv(newconn, recvbuf)) ERR_OK){taskENTER_CRITICAL(); /*进入临界区*//*数据接收缓冲区清零*/memset(lwip_demo_recvbuf, 0, LWIP_DEMO_RX_BUFSIZE);for (q recvbuf-p; q ! NULL; q q-next) /*遍历完整个pbuf链表*/{if (q-len (LWIP_DEMO_RX_BUFSIZE - data_len))memcpy(lwip_demo_recvbuf data_len, q-payload,(LWIP_DEMO_RX_BUFSIZE - data_len)); /* 拷贝数据**/elsememcpy(lwip_demo_recvbuf data_len, q-payload,q-len);data_len q-len;/* 超出TCP客户端接收数组,跳出*/if (data_len LWIP_DEMO_RX_BUFSIZE)break;}taskEXIT_CRITICAL(); /* 退出临界区*/data_len 0; /* 复制完成后data_len要清零*/netbuf_delete(recvbuf);}else if (recv_err ERR_CLSD) /* 关闭连接*/{netconn_close(newconn);netconn_delete(newconn);break;}}}} } 上述的源码结构和上一章节的TCPClient 实验非常相似它们唯一不同的是连接步骤不同。 下载验证 代码编译完成后下载到开发板中初始化完成之后我们来看一下LCD 显示的内容如下 图所示。 图18.2.3.1 LCD 显示。 我们在来看一下串口调试助手如图18.2.3.2 所示在串口调试助手上也输出了我们开发板 的IP 地址子网掩码、默认网关等信息。 图18.2.3.2 串口调试助手 我们通过网络调试助手发送数据到开发板当中结果如图18.2.3.3 所示当然我们可以通 过开发板上的KEY0 发送数据到网络调式助手当中如图18.2.3.4 所示 图18.2.3.3 LCD 显示 图18.2.3.4 网络调试助手接收数据 Socket 编程接口简介 lwIP 作者为了能更大程度上方便开发者将其他平台上的网络应用程序移植到lwIP 上也 为了能让更多开发者快速上手lwIP他设计了第三种应用程序编程接口即Socket API但 是该接口受嵌入式处理器资源和性能的限制部分Socket 接口并未在lwIP 中完全实现。 Socket 编程接口简介 说到Socket我们不得不提起BSD SocketBSD Socket 是由加州伯克利大学为Unix 系统 开发出来的所以被称为伯克利套接字Internet Berkeley SocketsBSD Socket 是采用C 语 言进程间通信库的应用程序接口API允许不同主机或者同一个计算机上的不同进程之间 的通信支持多种I/O 设备和驱动具体的实现是依赖操作系统的。这种接口对于TCP/IP 是 必不可少的所以是互联网的基础技术之一所以LWIP 也是引入该程序编程接口虽然不能 完全实现BSD Socket但是对于开发者来说已经足够了。 在lwIP 抽象出来的Socket API 中lwIP 内核为用户提供了最多NUM_SOCKETS 个可使 用的Socket 描述符并定义了结构体lwip_socket对netconn 结构的封装和增强来描述一个 具体连接。内核定义了数组Sockets通过一个Socket 描述符就可以索引得到相应的连接结构 lwip_socket从而实现对连接的操作。连接结构lwip_socket 的数据结构实现源码如下 #define NUM_SOCKETS MEMP_NUM_NETCONN #ifndef SELWAIT_T #define SELWAIT_T u8_t #endif union lwip_sock_lastdata {struct netbuf *netbuf;struct pbuf *pbuf; }; /* 包含套接字使用的所有内部指针和状态*/ struct lwip_sock {/* 套接字目前构建在网络上每个套接字有一个netconn */struct netconn *conn;/* 读上一次读取中留下的数据*/union lwip_sock_lastdata lastdata; #if LWIP_SOCKET_SELECT || LWIP_SOCKET_POLL/* 接收数据的次数由event_callback()设置通过接收和选择功能进行测试*/s16_t rcvevent;/* 数据被隔离的次数(发送缓冲区)由event_callback()设置测试选择*/u16_t sendevent;/* 这个套接字发生错误由event_callback()设置由select测试*/u16_t errevent;/* 使用select计算有多少线程正在等待这个套接字*/SELWAIT_T select_waiting; #endif /* LWIP_SOCKET_SELECT || LWIP_SOCKET_POLL */ #if LWIP_NETCONN_FULLDUPLEX/* 多少线程使用struct lwip_sock(不是int)的计数器*/u8_t fd_used;/* 挂起的关闭/删除操作的状态*/u8_t fd_free_pending; #define LWIP_SOCK_FD_FREE_TCP 1 #define LWIP_SOCK_FD_FREE_FREE 2 #endif }; lwip_socket 结构是对连接结构netconn 的再次封装在内核中它对lwip_socket 的操作最 终都会映射到对netconn 结构的操作上。简单来讲Socket API 完全依赖netconn 接口实现的。 Socket API 函数 (1) socket 函数 该函数的原型如下源码所示 #define socket(domain,type,protocol) lwip_socket(domain,type,protocol)向内核申请一个套接字本质上该函数调用了函数lwip_socket该函数的参数如下表所 示 (2) bind 函数 该函数的原型如下源码所示 #define bind(s,name,namelen) lwip_bind(s,name,namelen) int bind(int s, const struct sockaddr *name, socklen_t namelen)该函数与netconn_bind 函数一样用于服务器端绑定套接字与网卡信息本质上就是对函数netconn_bind 再一次封装从上述源码可以知道参数name 指向一个sockaddr 结构体它包 含了本地IP 地址和端口号等信息参数namelen 指出结构体的长度。结构体sockaddr 定义如 下源码所示 struct sockaddr {u8_t sa_len; /* 长度*/sa_family_t sa_family; /* 协议簇*/char sa_data[14]; /* 连续的14 字节信息*/ }; struct sockaddr_in {u8_t sin_len; /* 长度*/u8_t sin_family; /* 协议簇*/u16_t sin_port; /* 端口号*/struct in_addr sin_addr; /* IP地址*/char sin_zero[8]; }可以看出lwIP 作者定义了两个结构体结构体sockaddr 中的sa_family 指向该套接字所 使用的协议簇本地IP 地址和端口号等信息在sa_data 数组里面定义这里暂未用到。由于 sa_data 以连续空间的方式存在所以用户要填写其中的IP 字段和端口port 字段这样会比较 麻烦因此lwIP 定义了另一个结构体sockaddr_in它与sockaddr 结构对等只是从中抽出IP 地址和端口号port方便于用于的编程操作。 (3) connect 函数 该函数与netconn 接口的netconn_connect 函数作用是一样的因此它是被netconn_connect 函数封装了该函数的作用是将Socket 与远程IP 地址和端口号绑定如果开发板作为客户端 通常使用这个函数来绑定服务器的IP 地址和端口号对于TCP 连接调用这个函数会使客户 端与服务器之间发生连接握手过程并建立稳定的连接如果是UDP 连接该函数调用不会 有任何数据包被发送只是在连接结构中记录下服务器的地址信息。当调用成功时函数返回 0否则返回-1。该函数的原型如下源码所示 #define connect(s,name,namelen) lwip_connect(s,name,namelen) int lwip_connect(int s, const struct sockaddr *name, socklen_t namelen);(4) listen 函数 该函数和netconn 的函数netconn_listen 作用一样它是由函数netconn_listen 封装得来的 内核同时接收到多个连接请求时需要对这些请求进行排队处理参数backlog 指明了该套接 字上连接请求队列的最大长度。当调用成功时函数返回0否则返回-1。该函数的原型如下 源码所示 #define listen(s,backlog) lwip_listen(s,backlog) int lwip_listen(int s, int backlog);注意该函数作用于TCP 服务器程序。 (5) accept 函数 该函数与netconn_accept 作用是一样的当接收到新连接后连接另一端客户端的地 址信息会被填入到地址结构addr 中而对应地址信息的长度被记录到addrlen 中。函数返回新 连接的套接字描述符若调用失败函数返回-1。该函数的原型如下源码所示 #define accept(s,addr,addrlen) lwip_accept(s,addr,addrlen) int lwip_accept(int s, struct sockaddr *addr, socklen_t *addrlen);注意该函数作用于TCP 服务器程序。 (6) send()/sendto()函数 该函数是被netconn_send 封装的其作用是向另一端发送UDP 报文这两个函数的原型 如下源码所示 #define send(s,dataptr,size,flags) lwip_send(s,dataptr,size,flags) #define sendto(s,dataptr,size,flags,to,tolen) lwip_sendto(s,dataptr,size,flags,to,tolen) ssize_t lwip_send(int s, const void *dataptr, size_t size, int flags); ssize_t lwip_sendto(int s, const void *dataptr, size_t size, int flags, const struct sockaddr *to, socklen_t tolen);可以看出函数sendto 比函数send 多了两个参数该函数如下表所示 (7) write 函数 该函数用于在一条已经建立的连接上发送数据通常使用在TCP 程序中但在UDP 程序 中也能使用。该函数本质上是基于前面介绍的send 函数来实现的其参数的意义与send 也相 同。当函数调用成功时返回成功发送的字节数否则返回-1。 (8) read()/recv()/recvfrom()函数 函数recvfrom 和recv 用来从一个套接字中接收数据该函数可以在UDP 程序使用也可 在TCP 程序中使用。该函数本质上是被函数netconn_recv 的封装其参数与函数sendto 的参 数完全相似如表20.2.3 所示数据发送方的地址信息会被填写到from 中fromlen 指明了缓 存from 的长度mem 和len 分别记录了接收数据的缓存起始地址和缓存长度flags 指明用户 控制接收的方式通常设置为0。两个函数的原型如下源码所示 #define recv(s, mem, len, flags) lwip_recv(s, mem, len, flags) #define recvfrom(s, mem, len, flags, from, fromlen) lwip_recvfrom(s, mem, len, flags, from, fromlen) ssize_t lwip_readv(int s, const struct iovec *iov, int iovcnt); ssize_t lwip_recvfrom(int s, void *mem, size_t len, int flags,struct sockaddr *from, socklen_t *fromlen); #define read(s, mem, len) lwip_read(s, mem, len) ssize_t lwip_read(int s,void *mem,size_t len); (9) close 函数 函数close 作用是关闭套接字对应的套接字描述符不再有效与描述符对应的内核结构 lwip_Socket 也将被全部复位。该函数本质上是被netconn_delete 的封装对于TCP 连接来说 该函数将导致断开握手过程的发生。若调用成功该函数返回0否则返回-1。该函数的原型 如下源码所示 #define close(s) lwip_close(s) int lwip_close(int s);这些函数到底如何调用请大家看第十五章节的内容这里已经很详细讲解了 API消息如何发送到内核并调用相关的函数。 Socket 编程接口UDP 实验 对于lwIP 的Socket 的使用方式其实和文件操作相类似的。如果我们打开一个文件首先 打开-读/写-关闭在TCP/IP 网络通信中同样存在这些操作不过它使用的接口不是文件描述 符或者FILE*而是一个称做Socket 的描述符。对于Socket它也可以通过读、写、打开、 关闭操作来进行网络数据传送。同时还有一些辅助的函数如域名/IP 地址查询、Socket 功 能设置等。本章我们使用Scokrt 编程接口实现UDP 实验。本章分为如下几个部分 20.1 Socket 编程UDP 连接流程 20.2 Socket 接口的UDP 实验 Socket 编程UDP 连接流程 实现UDP 协议之前用户必须先配置结构体sockaddr_in 的成员变量才能实现UDP 连接 该配置步骤如下所示 ①sin_family 设置为AF_INET 表示IPv4 网络协议。 ②sin_port 为设置端口号笔者设置为8080。 ③sin_addr.s_addr 设置本地IP 地址。 ④调用函数Socket 创建Socket 连接注意该函数的第二个参数SOCK_STREAM 表 示TCP 连接SOCK_DGRAM 表示UDP 连接。 ⑤调用函数bind 将本地服务器地址与Socket 进行绑定。 ⑥调用收发函数接收或者发送。 Socket 接口的UDP 实验 20.2.1 硬件设计 例程功能 本实验使用Socket 编程接口实现UDP 服务器并可通过按键发送UDP 广播数据至其他 的UDP 客户端也能够接收其他UDP 客户端广播的数据并实时显示至LCD 屏幕上。 该实验的实验工程请参考《lwIP 例程10 lwIP_SOCKET_UDP 实验》。 20.2.2 软件设计 20.2.2.1 程序流程图 本实验的程序流程图如下图所示 程序解析 在本章节中我们最主要关注两个文件分别为lwip_demo.c 和lwip_demo.h 文件 lwip_demo.h 文件主要定义了发送标志位以及声明lwip_demo 函数这里比较简单我们不需要 去讲解主要看lwip_demo.c 文件的函数我们在lwip_demo 函数中配置相关UDP 参数以及 创建了一个发送数据线程lwip_send_thread这个发送线程就是调用scokec 函数发送数据到服 务器中下面我们分别地讲解以下lwip_demo 函数以及lwip_send_thread 任务如下源码所示 /*** brief 发送数据线程* param 无* retval 无*/ void lwip_data_send(void) {sys_thread_new(lwip_send_thread, lwip_send_thread, NULL,512, LWIP_SEND_THREAD_PRIO); } /*** brief lwip_demo实验入口* param 无* retval 无*/ void lwip_demo(void) {BaseType_t lwip_err;lwip_data_send(); /* 创建发送数据线程*/memset(local_info, 0, sizeof(struct sockaddr_in)); /* 将服务器地址清空*/local_info.sin_len sizeof(local_info);local_info.sin_family AF_INET; /* IPv4地址*/local_info.sin_port htons(LWIP_DEMO_PORT); /* 设置端口号*/local_info.sin_addr.s_addr htons(INADDR_ANY); /* 设置本地IP地址*/sock_fd Socket(AF_INET, SOCK_DGRAM, 0); /* 建立一个新的Socket连接*//* 建立绑定*/bind(sock_fd, (struct sockaddr *)local_info, sizeof(struct sockaddr_in));while (1){memset(lwip_demo_recvbuf, 0, sizeof(lwip_demo_recvbuf));recv(sock_fd, (void *)lwip_demo_recvbuf, sizeof(lwip_demo_recvbuf), 0);lwip_err xQueueSend(Display_Queue, lwip_demo_recvbuf, 0);if (lwip_err errQUEUE_FULL){printf(队列Key_Queue已满数据发送失败!\r\n);}} } /*** brief 发送数据线程函数* param pvParameters : 传入参数(未用到)* retval 无*/ void lwip_send_thread(void *pvParameters) {pvParameters pvParameters;local_info.sin_addr.s_addr inet_addr(IP_ADDR); /* 需要发送的远程IP地址*/while (1){if ((lwip_send_flag LWIP_SEND_DATA) LWIP_SEND_DATA){sendto(sock_fd, /* Socket */(char *)lwip_demo_sendbuf, /* 发送的数据*/sizeof(lwip_demo_sendbuf), 0, /* 发送的数据大小*/(struct sockaddr *)local_info, /* 接收端地址信息*/sizeof(local_info)); /* 接收端地址信息大小*/lwip_send_flag ~LWIP_SEND_DATA;}vTaskDelay(100);} } 从上述的源码可知笔者在lwip_demo 函数中调用lwip_data_send 函数创建 “lwip_send_thread”发送数据线程创建发送任务之后配置Socket 的UDP 协议这个配置流 程笔者已经在21.1 小节讲解过了这里无需重复讲解该函数的while()循环主要调用recv 函 数获取数据并使用消息队列发送。至于lwip_send_thread 发送数据数据线程函数非常简单它 主要判断发送标志位是否有效如果标志位有效则程序调用sendto 发送数据并设置标志位 无效。 注意函数recv 一般处于阻塞状态当然它可以设置为非阻塞。在lwip_send_thread 线程 中我们还需要执行“local.sin_addr.s_addr inet_addr(IP_addr)”这段代码因为我们必须知 道数据发送到哪里所以宏定义IP_addr 需要根据自己的远程IP 地址来设置。 下载验证 打开串口调试助手和网络调试助手注意必须查看PC 机上的IP 地址为多少才能确定程 序的宏定义IP_addr 数值如下源码所示 #define IP_addr 192.168.1.37 /* 远程IP */代码编译完成后下载到开发板中等待lwIP 一系列初始化和等待DHCP 分配IP 地址下 面我们来看一下LCD 显示的内容如图20.2.3.1 所示 我们在来看一下串口调试助手如图20.2.3.2 所示在串口调试助手上也输出了我们开发板 的IP 地址子网掩码、默认网关等信息。 图20.2.3.2 串口调试助手 我们通过网络调试助手发送数据到开发板当中结果如图20.2.3.3 所示当然我们可以通 过开发板上的KEY0 发送数据到网络调式助手当中如图20.2.3.4 所示 图20.2.3.3 LCD 显示 图20.2.3.4 网络调试助手接收数据 Socket 编程接口TCP 服务器实验 关于TCP 协议的相关知识请参考第12 章的内容。本章笔者重点讲解lwIP 的Socket 接口如何配置TCP 服务器并在此基础上实现收发功能。本章分为如下几个部分 22.1 Socket 编程TCP 服务器流程 22.2 Socket 接口的TCPServer 实验 Socket 编程TCP 服务器流程 实现TCP 服务器之前用户必须先配置结构体sockaddr_in 的成员变量才能实现 TCPServer 连接该配置步骤如下所示 ①sin_family 设置为AF_INET 表示IPv4 网络协议。 ②sin_port 为设置端口号。 ③sin_addr.s_addr 设置本地IP 地址。 ④调用函数Socket 创建Socket 连接注意该函数的第二个参数SOCK_STREAM 表 示TCP 连接SOCK_DGRAM 表示UDP 连接。 ⑤调用函数bind 绑定本地IP 地址和端口号。 ⑥调用函数listen 监听连接请求。 ⑦调用函数accept 监听连接。 ⑧调用收发函数进行通讯。 上述的步骤就是Socket 编程接口配置TCPServer 的流程。 Socket 接口的TCPServer 实验 硬件设计 例程功能 本实验使用Socket 编程接口实现TCP 服务器并可通过按键向连接的TCP 客户端发送数 据也能够接收来自TCP 客户端的数据并实时显示至LCD 屏幕上。 该实验的实验工程请参考《lwIP 例程12 lwIP_SOCKET_TCPServer 实验》。 软件设计 22.2.2.1 程序流程图 本实验的程序流程图如下图所示 程序解析 本实验我们着重讲解lwip_demo.c 文件该文件实现了三个函数它们分别为 lwip_data_send、lwip_demo 和lwip_send_thread 函数下面笔者分别地讲解它们的实现功能。 /*** brief 发送数据线程* param 无* retval 无*/ void lwip_data_send(void) {sys_thread_new(lwip_send_thread, lwip_send_thread, NULL,512, LWIP_SEND_THREAD_PRIO); } 此函数调用sys_thread_new 函数创建发送数据线程它的线程函数为lwip_send_thread 稍后我们重点会讲解。 /*** brief lwip_demo实验入口* param 无* retval 无*/ void lwip_demo() {struct sockaddr_in server_addr; /* 服务器地址*/struct sockaddr_in conn_addr; /* 连接地址*/socklen_t addr_len; /* 地址长度*/int err;int length;int sock_fd;char *tbuf;BaseType_t lwip_err;lwip_data_send(); /* 创建一个发送线程*/sock_fd Socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);memset(server_addr, 0, sizeof(server_addr)); /* 将服务器地址清空*/server_addr.sin_family AF_INET; /* 地址家族*/server_addr.sin_addr.s_addr htonl(INADDR_ANY); /* 注意转化为网络字节序*//* 使用SERVER_PORT指定为程序头设定的端口号*/server_addr.sin_port htons(LWIP_DEMO_PORT);tbuf mymalloc(SRAMIN, 200); /* 申请内存*/sprintf((char *)tbuf, Port:%d, LWIP_DEMO_PORT); /* 客户端端口号*/lcd_show_string(5, 150, 200, 16, 16, tbuf, BLUE);/* 建立绑定*/err bind(sock_fd, (struct sockaddr *)server_addr, sizeof(server_addr));if (err 0) /* 如果绑定失败则关闭套接字*/{closeSocket(sock_fd); /* 关闭套接字*/myfree(SRAMIN, tbuf);}err listen(sock_fd, 4); /* 监听连接请求*/if (err 0) /* 如果监听失败则关闭套接字*/{closeSocket(sock_fd); /* 关闭套接字*/}while (1){lwip_connect_state 0;addr_len sizeof(struct sockaddr_in); /* 将链接地址赋值给addr_len *//* 对监听到的请求进行连接状态赋值给sock_conn */sock_conn accept(sock_fd, (struct sockaddr *)conn_addr, addr_len);if (sock_conn 0) /* 状态小于0代表连接故障此时关闭套接字*/{closeSocket(sock_fd);}else{lwip_connect_state 1;}while (1){length recv(sock_conn, (unsigned int *)lwip_demo_recvbuf,sizeof(lwip_demo_recvbuf), 0);if (length 0){goto atk_exit;}lwip_err xQueueSend(Display_Queue, lwip_demo_recvbuf, 0);if (lwip_err errQUEUE_FULL){printf(队列Key_Queue已满数据发送失败!\r\n);}}atk_exit:if (sock_conn 0){closeSocket(sock_conn);sock_conn -1;lcd_fill(5, 89, lcddev.width, 110, WHITE);lcd_show_string(5, 90, 200, 16, 16, State:Disconnect, BLUE);myfree(SRAMIN, tbuf);}} } 根据22.1 小节的流程配置server_addr 结构体的字段配置完成之后调用listen 和accept 监 听客户端连接请求接着调用recv 函数接收客户端的数据并且把数据以消息的方式发送至 其他线程当中。 /*** brief 发送数据线程函数* param pvParameters : 传入参数(未用到)* retval 无*/ static void lwip_send_thread(void *pvParameters) {pvParameters pvParameters;while (1){if (((lwip_send_flag LWIP_SEND_DATA) LWIP_SEND_DATA) (lwip_connect_state 1)) /* 有数据要发送*/{/* 发送数据*/send(sock_conn, lwip_demo_sendbuf, sizeof(lwip_demo_sendbuf), 0);lwip_send_flag ~LWIP_SEND_DATA;}vTaskDelay(10);} } 此线程函数非常简单它主要判断lwip_send_flag 变量的状态若该变量的状态为发送状 态则程序调用send 函数发送数据并且清除lwip_send_flag 变量的状态。 下载验证 初始化完成之后LCD 显示以下信息如下图所示 我们通过网络调试助手发送数据至开发板开发板接收完成之后LCD 在指定位置显示接 收的数据如下图所示 图22.2.3.2 LCD 显示 当然读者可通过KEY0 按键发送数据至网络调试助手。
http://www.zqtcl.cn/news/364871/

相关文章:

  • 品牌建设+网站网站建设 淘宝客末班
  • 建设商业网站学校建设门户网站的好处
  • 男女朋友在一起做那个的网站公司建设网站
  • 营销型网站的类型有哪些相册网站怎么做
  • 河南建设监理协会网站电话erp管理系统官网
  • 视频网站seo实战做企业网站一般用什么服务器
  • icp备案 网站负责人免费直播sdk
  • 网站制作和如何推广动画专业学什么
  • 北京一家专门做会所的网站基于ssh框架的网站开发流程
  • 可以在自己的电脑上做网站吗陕西商城网站建设
  • 深圳网站建设工作室郴州有什么好玩的地方
  • 用dw做的网站怎么发到网上竹妃怎么在公众号里做网站
  • 杭州网站优化搜索黑龙江公共资源交易网官网
  • 动易网站 首页模板修改平台网站是什么
  • 营销网站更受用户欢迎的原因是英文网站的建设意义
  • 学网站建设基础河北省建设网站的网站首页
  • 铜仁市住房和城乡建设部网站延边有没有做网站的
  • 如何做ppt的模板下载网站移动端网站的优点
  • 网站域名没有实名认证微信平台开发技术
  • 自己用电脑做虚拟机怎么建网站个人网站 icp 代理
  • 嘉兴网站建设999 999中国建设招标网是私人网站吗
  • 网站程序设置主页面零基础学wordpress pdf下载
  • 网站代码优化有哪些专做立体化的网站
  • 单县网站定制培训机构专业
  • 网站防红链接怎么做网站建设中提示页面
  • 网站开发和游戏开发的区别互联网服务平台投诉
  • 杭州定制网站公司出名的设计网站
  • 网站查询访问注册电气工程师考试
  • 北京企业网站推广哪家公司好电商平台代运营
  • 北京快速建站模板信息管理系统网站开发