18. ESP32的WiFi应用
一、TCP/IP协议栈
ESP32 S3 是一款集成了 Wi-Fi 和蓝牙功能的微控制器,而 lwIP(轻量级 IP)是一个为嵌入式系统设计的开源 TCP/IP 协议栈。通过使用 lwIP 库,ESP32-S3 可以实现与外部网络的通信,包括发送和接收数据包、处理网络连接等。因此,ESP32 S3 是基于 lwIP 来实现网络功能的。
1.1、什么是TCP/IP协议栈
TCP/IP 协议栈是一系列网络协议的总和,构成网络通信的核心骨架,定义了电子设备如何连入因特网以及数据如何在它们之间进行传输。该协议采用 4 层结构,分别是 应用层、传输层、网络层 和 网络接口层,每一层都使用下一层提供的协议来完成自己的需求。
- 应用层:这是最顶层,负责处理特定的应用程序细节。在这一层,用户的数据被处理和解释。一些常见的应用层协议包括 HTTP、FTP、SMTP 和 DNS 等。
- 传输层:这一层负责数据包的分割、打包以及传输控制,确保数据能够可靠、有序地到达目的地。主要的传输层协议有 TCP 和 UDP。
- 网络层:负责确定数据包的路径从源到目的地。这一层的主要协议是 IP(Internet Protocol),它负责在主机之间发送和接收数据包。
- 网络接口层:这是最底层,负责将数据转换为可以在物理媒介上发送的信号。这一层的协议涉及到如何将数据帧封装在数据链路层,以便在网络上进行传输。
每一层都使用下一层提供的服务,同时对上一层提供服务。这种分层结构使得协议栈更加灵活,易于扩展和维护。不同层次上的协议一起工作,协调数据在计算机网络中的传输,使得不同的计算机能够相互通信。
TCP/IP 协议栈和传统的 OSI 模型并不完全对应。TCP/IP 协议栈是一个简化的模型,强调了实际的协议实现和因特网的实际运作方式。相比之下,OSI 模型更加全面和理想化,它提供了一个框架来描述不同系统之间的交互方式。
ISO/OSI 分层模型也是一个分层结构,包括七个层次,从上到下分别是:应用层、表示层、会话层、传输层、网络层、数据链路层 和 物理层。虽然 ISO/OSI 模型为不同的系统之间的通信提供了一个理论框架,但 TCP/IP 协议栈更侧重于实际的协议实现和因特网的实际运作方式。网络技术的发展并不是遵循严格的 ISO/OSI 分层概念。实际上现在的互联网使用的是 TCP/IP 体系结构中某些应用程序可以直接使用 IP 层,或甚至直接使用最下面的网络接口层。
无论那种表示方法,TCP/IP 模型各个层次都分别对应于不同的协议。TCP/IP 协议栈负责确保网络设备之间能够通信。它是一组规则,规定了信息如何在网络中传输。其中,这些协议都分布在应用层,传输层和网络层,网络接口层是由硬件来实现。如 Windows 操作系统包含了 CBISC 协议栈,该协议栈就是实现了 TCP/IP 协议栈的应用层,传输层和网络层的功能,网络接口层由网卡实现,所以 CBISC 协议栈和网卡构建了网络通信的核心骨架。因此,无论哪一款以太网产品,都必须符合 TCP/IP 体系结构,才能实现网络通信。注意:路由器和交换机等相关网络设备只实现网络层和网络接口层的功能。
1.2、TCP/IP协议栈的封包和拆包
TCP/IP 协议栈的封包和拆包是指在网络通信中,将数据按照一定的协议和格式进行封装和解析的过程。
在 TCP/IP 协议栈中,数据封装 是指在发送端将数据按照协议规定的格式进行打包,以便在网络中进行传输。在应用层的数据被封装后,会经过传输层、网络层和网络接口层的处理,最终转换成可以在物理网络上传输的帧格式。数据封装的过程涉及到对数据的分段、压缩、加密等操作,以确保数据能够可靠、安全地传输到目的地,下图描述的是封包处理流程。
数据拆包 是指接收端收到数据后,按照协议规定的格式对数据进行解析和处理,还原出原始的数据。在接收到数据后,接收端会按照协议规定的层次从下往上逐层处理数据,最终将应用层的数据还原出来。数据拆包的过程涉及到对数据的重组、解压缩、解密等操作,以确保数据能够被正确地解析和处理,下图描述的是拆包处理流程。
TCP/IP 协议栈的封包和拆包过程涉及到多个层次和协议的处理,需要按照协议规定的格式和顺序进行操作。在实际应用中,需要根据具体的情况选择合适的协议和格式来满足不同的需求。同时,为了保证数据的安全和可靠性,还需要采取相应的加密、压缩等措施,以避免数据被篡改或损坏。
封包时,数据添加各层协议的首部,拆包时,在各层间除去自层的首部。
二、LwIP
2.1、什么是LwIP
LwIP,全称为 Lightweight IP 协议,是一种专为嵌入式系统设计的轻量级 TCP/IP 协议栈。它可以在无操作系统或带操作系统环境下运行,支持多线程或无线程,适用于 8 位和 32 位微处理器,同时兼容大端和小端系统。它的设计核心理念在于保持 TCP/IP 协议的主要功能同时尽量减少对 RAM 的占用。这意味着,尽管它的体积小巧,但它能够实现完整的 TCP/IP 通信功能。
通常,LwIP 只需十几 KB 的 RAM 和大约 40K 的 ROM 即可运行,使其成为资源受限的嵌入式系统的理想选择。LwIP 的灵活性使其既可以在无操作系统环境下工作,也可以与各种操作系统配合使用。这为开发者提供了更大的自由度,可以根据具体的应用需求和硬件配置进行优化。无论是在云台接入、无线网关、远程模块还是工控控制器等场景中,LwIP 都能提供强大的网络支持。
2.2、LwIP与TCP/IP体系结构的对应关系
从上图可以清晰地看到,LwIP 软件库主要实现了 TCP/IP 体系结构中的三个层次:应用层、传输层 和 网络层。这些层次共同处理和传输数据包,确保了数据在网络中的可靠传输。然而,网络接口层作为 TCP/IP 协议栈的最底层,其功能并无法通过软件方式完全实现。
网络接口层 的主要任务是 将数据包转换为光电模拟信号,以便能够在物理媒介上传输。这个过程涉及到与硬件的直接交互,包括数据的调制解调、信号的转换等,这些都是软件难以模拟或实现的。因此,虽然 LwIP 软件库没有实现网络接口层的功能,但通过与底层硬件的紧密配合,它仍然能够提供完整且高效的 TCP/IP 通信功能。这也使得 LwIP 成为一个适用于资源受限的嵌入式系统的理想选择。
三、WiFi MAC内核简介
ESP32 S3 完全遵循 802.11b/g/n Wi-Fi MAC 协议栈,支持分布式控制功能(DCF)下的基本服务集(BSS)STA 和 SoftAP 操作。支持通过最小化主机交互来优化有效工作时长,以实现功耗管理。ESP32 S3 WiFi MAC 支持 4 个虚拟 WiFi 接口,同时支持基础结构型网络、SoftAP 模式和 Station + SoftAP 混杂模式。它还具备 RTS 保护、CTS 保护、立即块确认、分片和重组、TX/RX A-MPDU 和 TX/RX A-MSDU 等高级功能。此外,ESP32 S3 还支持无线多媒体、GCMP、CCMP、TKIP、WAPI 等安全协议,并提供自动 Beacon 监测和 802.11mc FTM 支持。
从上图可以看出,ESP32 S3 芯片内置 WiFi MAC 内核。当我们发送数据到网络时,数据首先被转化为无线信号,然后发送到该设备连接的 WiFi 路由器中。接着,路由器通过网线将数据传输到目标主机,从而完成数据传输操作。
- 数据转化为无线信号:当 ESP32 S3 想要发送数据到网络时,它首先会将数据封装到一个无线传输帧中。这一过程涉及到将数据转化为可以在无线介质上传输的格式。
- 发送到 WiFi 路由器:封装后的无线信号然后被发送到 ESP32 S3 连接的 WiFi 路由器。WiFi 路由器充当一个中间设备,负责将无线信号转换为有线网络信号(如果目标主机是通过有线网络连接的)或直接转发无线信号(如果目标主机也是通过 WiFi 连接的)。
- 路由器传输数据:WiFi 路由器接收到无线信号后,会进一步处理它。如果目标主机是通过有线网络连接的,路由器会将无线信号转换为有线网络信号,并通过网线将其传输到目标主机。如果目标主机也是通过 WiFi 连接的,路由器会直接转发无线信号到目标主机。
- 完成数据传输:最终,目标主机接收到路由器发送的有线网络信号或无线信号,并将其解析为原始数据。这样,整个数据传输过程就完成了。
在整个过程中,ESP32 S3 的 WiFi MAC 内核起着核心的作用,它负责管理无线连接、封装和解封装数据以及与 WiFi 路由器进行通信。
四、lwIP Socket编程接口
LwIP 作者为了方便开发者将其他平台上的网络应用程序移植到 LwIP 上,并让更多开发者快速上手 LwIP,作者设计了三种应用程序编程接口:RAW 编程接口、NETCONN 编程接口 和 Socket 编程接口。然而,由于 RAW 编程接口只能在无操作系统环境下运行,因此对于内嵌 FreeRTOS 操作系统的 ESP32 来说,无法使用这个编程接口。
Socket 编程接口 是由 NETCONN 编程接口封装而成,但是该接口非常简易的实现网络连接。需要注意的是,由于受到嵌入式处理器资源和性能的限制,部分 Socket 接口并未在 LwIP 中完全实现。因此,为了实现网络连接,推荐使用 Socket API。
4.1、创建套接字
我们可以使用 socket()
函数向内核 申请一个套接字,本质上该函数调用了函数 lwip_socket()
,该函数的原型如下所示:
/**
* @brief 申请套接字函数
*
* @param domain 创建的套接字指定使用的协议簇
* @param type 协议簇中的具体服务类型
* @param protocol 实际使用的具体协议,若设置为"0",表示根据前两个参数使用缺省协议
* @return int 创建的套接字的索引
*/
static inline int socket(int domain, int type, int protocol)
{
return lwip_socket(domain, type, protocol);
}
int lwip_socket(int domain, int type, int protocol);
形参 domain
是 创建的套接字指定使用的协议簇,它的可选值如下:
#define AF_INET 2 // 表示IPv4网络协议
#define AF_INET6 10 // 表示IPv6
#define AF_UNIX 1 // 表示本地套接字(使用一个文件)
形参 type
是 协议簇中的具体服务类型,它的可选值如下:
#define SOCK_STREAM 1 // 可靠数据流交付服务,比如TCP
#define SOCK_DGRAM 2 // 无连接数据报交付服务,比如UDP
#define SOCK_RAW 3 // 原始套接字,比如RAW
形参 protocol
是 实际使用的具体协议,它的可选值如下:
#define IPPROTO_IP 0
#define IPPROTO_ICMP 1
#define IPPROTO_TCP 6
#define IPPROTO_UDP 17
#define IPPROTO_IPV6 41
#define IPPROTO_ICMPV6 58
#define IPPROTO_UDPLITE 136
#define IPPROTO_RAW 255
4.2、绑定套接字与网卡信息
我们可以使用 bind()
函数 绑定套接字与网卡信息,本质上该函数调用了函数 lwip_bind()
,该函数的原型如下所示:
/**
* @brief 绑定套接字与网卡信息函数
*
* @param s 套接字
* @param name 包含本地IP地址和端口号等信息的结构体指针
* @param namelen 结构体的长度
* @return int 0: 成功; -1: 失败;
*/
static inline int bind(int s, const struct sockaddr *name, socklen_t namelen)
{
return lwip_bind(s, name, namelen);
}
int lwip_bind(int s, const struct sockaddr *name, socklen_t namelen);
形参 name
是指向一个 sockaddr
结构体指针,它包含了 本地 IP 地址和端口号等信息。
typedef uint8_t u8_t;
typedef u8_t sa_family_t;
struct sockaddr
{
u8_t sa_len; // 长度
sa_family_t sa_family; // 协议族
char sa_data[14]; // 连续的14字节信息,定义了本地IP地址和端口号等信息
};
lwIP 作者定义了两个结构体,结构体 sockaddr
中的 sa_family
指向该 套接字所使用的协议簇,本地 IP 地址和 端口号等信息在 sa_data
数组里面定义。由于 sa_data
以连续空间的方式存在,所以用户要填写其中的 IP 字段和端口 port 字段,这样会比较麻烦,因此 lwIP 定义了另一个结构体 sockaddr_in
,它与 sockaddr
结构对等,只是从中抽出 IP 地址和端口号 port,方便于用于的编程操作。
typedef __uint32_t in_addr_t;
struct in_addr
{
in_addr_t s_addr;
};
struct sockaddr_in
{
u8_t sin_len; // 长度
sa_family_t sin_family; // 协议簇
in_port_t sin_port; // 端口号
struct in_addr sin_addr; // IP地址
#define SIN_ZERO_LEN 8
char sin_zero[SIN_ZERO_LEN];
};
4.3、服务端监听客户端
我们可以使用 listen()
函数的 监听客户端,本质上该函数调用了函数 lwip_listen()
,该函数的原型如下所示:
/**
* @brief 服务端监听客户端
*
* @param s 套接字
* @param backlog 套接字上连接请求队列的最大长度
* @return int 0: 成功; -1: 失败;
*/
static inline int listen(int s, int backlog)
{
return lwip_listen(s, backlog);
}
int lwip_listen(int s, int backlog);
此函数作用于 TCP 服务器程序。
4.4、客户端连接服务器
我们可以使用 connect()
函数的 连接服务器,本质上该函数调用了函数 lwip_connect()
,该函数的原型如下所示:
/**
* @brief 客户端连接服务器
*
* @param s 套接字
* @param name 包含服务端IP地址和端口号等信息的结构体指针
* @param namelen 结构体的长度
* @return int 0: 成功; -1: 失败;
*/
static inline int connect(int s, const struct sockaddr *name, socklen_t namelen)
{
return lwip_connect(s, name, namelen);
}
lwip_connect(int s, const struct sockaddr *name, socklen_t namelen);
对于 TCP 连接,调用这个函数会使客户端与服务器之间发生连接握手过程,并建立稳定的连接;如果是 UDP 连接,该函数调用不会有任何数据包被发送,只是在连接结构中记录下服务器的地址信息。当调用成功时,函数返回 0;否则返回 -1。
4.5、服务端接收客户端连接
我们可以使用 accept()
函数的 服务端接收客户端连接,本质上该函数调用了函数 lwip_accept()
,该函数的原型如下所示:
/**
* @brief 服务端接收客户端连接
*
* @param s 套接字
* @param addr 保存客户端的地址信息
* @param addrlen 客户端的地址信息的长度
* @return int 0: 成功; -1: 失败;
*/
static inline int accept(int s, struct sockaddr *addr, socklen_t *addrlen)
{
return lwip_accept(s, addr, addrlen);
}
int lwip_accept(int s, struct sockaddr *addr, socklen_t *addrlen);
此函数作用于 TCP 服务器程序。
4.6、发送数据
在 TCP 连接中,我们可以使用 send()
函数的 发送数据,本质上该函数调用了函数 lwip_send()
,该函数的原型如下所示:
/**
* @brief 发送数据
*
* @param s 套接字
* @param dataptr 发送数据的起始地址
* @param size 长度
* @param flags 数据发送时的特殊处理,例如带外数据、紧急数据等,通常设置为0
* @return ssize_t 发送数据的长度
*/
static inline ssize_t send(int s, const void *dataptr, size_t size, int flags)
{
return lwip_send(s, dataptr, size, flags);
}
ssize_t lwip_send(int s, const void *data, size_t size, int flags);
在 UDP 连接中,我们可以使用 sendto()
函数的 发送数据,本质上该函数调用了函数 lwip_sendto()
,该函数的原型如下所示:
/**
* @brief 发送数据
*
* @param s 套接字
* @param dataptr 发送数据的起始地址
* @param size 长度
* @param flags 数据发送时的特殊处理,例如带外数据、紧急数据等,通常设置为0
* @param to 目的地址信息
* @param tolen 目的地址信息长度
* @return ssize_t 发送数据的长度
*/
static inline ssize_t sendto(int s, const void *dataptr, size_t size, int flags, const struct sockaddr *to, socklen_t tolen)
{
return lwip_sendto(s, dataptr, size, flags, to, tolen);
}
ssize_t lwip_sendto(int s, const void *data, size_t size, int flags, const struct sockaddr *to, socklen_t tolen);
4.7、接收数据
在 TCP 连接中,我们可以使用 recv()
函数的 读取数据,本质上该函数调用了函数 lwip_recv()
,该函数的原型如下所示:
/**
* @brief 读取数据
*
* @param s 套接字
* @param mem 接收数据的缓存起始地址
* @param len 缓存长度
* @param flags 用户控制接收的方式,通常设置为0
* @return ssize_t 读取数据的长度
*/
static inline ssize_t recv(int s, void *mem, size_t len, int flags)
{
return lwip_recv(s, mem, len, flags);
}
ssize_t lwip_recv(int s, void *mem, size_t len, int flags);
在 UDP 连接中,我们可以使用 recvfrom()
函数的 读取数据,本质上该函数调用了函数 lwip_recvfrom()
,该函数的原型如下所示:
/**
* @brief 读取数据
*
* @param s 套接字
* @param mem 接收数据的缓存起始地址
* @param len 缓存长度
* @param flags 用户控制接收的方式,通常设置为0
* @param from 发送方的地址信息
* @param fromlen 保存发送方的地址信息长度
* @return ssize_t 读取数据的长度
*/
static inline ssize_t recvfrom(int s, void *mem, size_t len, int flags, struct sockaddr *from, socklen_t *fromlen)
{
return lwip_recvfrom(s, mem, len, flags, from, fromlen);
}
ssize_t lwip_recvfrom(int s, void *mem, size_t len, int flags, struct sockaddr *from, socklen_t *fromlen);
4.8、关闭套接字
我们可以使用 closesocket()
函数的 关闭套接字,本质上该函数调用了函数 lwip_close()
,该函数的原型如下所示:
/**
* @brief 关闭套接字
*
* @param s 套接字
* @return int 0: 成功; -1: 失败;
*/
static inline int closesocket(int s)
{
return lwip_close(s);
}
int lwip_close(int s);
五、WiFi模式概述
WiFi 主要有两种模式:STA 模式 和 AP 模式。AP 模式即无线接入点,被其他设备连接,也就是我们常说的手机热点;STA 模式即 Station,是连接热点的设备。另外,ESP32 S3 可支持 STA 和 AP 两种模式共存,就像手机那样可以开热点,也可以连接其他热点。
ESP IDF 提供了一套 API 来驱动 WiFi 功能。要使用此功能,我们需要在 CMakeLists.txt 文件中导入 esp_wifi
依赖库,然后还需要导入必要的头文件:
# 注册组件到构建系统的函数
idf_component_register(
# 依赖库的路径
REQUIRES esp_wifi
)
#include "esp_wifi.h"
5.1、扫描WiFi例程
我们在【components】文件夹下的【peripheral】文件夹下的【inc】文件夹(用来存放头文件)新建一个 bsp_wifi.h
文件,在【components】文件夹下的【peripheral】文件夹下的【src】文件夹(用来存放源文件)新建一个 bsp_wifi.c
文件。
#ifndef __BSP_WIFI_H__
#define __BSP_WIFI_H__
#include <stdio.h>
#include <string.h>
#include "esp_wifi.h"
#define DEFAULT_SCAN_WIFI_LIST_COUNT 12 // 默认扫描WiFi列表的个数
void print_auth_mode(int authmode);
void print_cipher_type(int pairwise_cipher, int group_cipher);
void bsp_wifi_scan(void);
#endif // !__BSP_WIFI_H__
#include "bsp_wifi.h"
/**
* @brief 打印身份认证模式函数
*
* @param authmode 身份验证模式
*/
void print_auth_mode(int authmode)
{
switch (authmode)
{
case WIFI_AUTH_OPEN:
printf("Authmode: WIFI_AUTH_OPEN\n");
break;
case WIFI_AUTH_OWE:
printf("Authmode: WIFI_AUTH_OWE\n");
break;
case WIFI_AUTH_WEP:
printf("Authmode: WIFI_AUTH_WEP\n");
break;
case WIFI_AUTH_WPA_PSK:
printf("Authmode: WIFI_AUTH_WPA_PSK\n");
break;
case WIFI_AUTH_WPA2_PSK:
printf("Authmode: WIFI_AUTH_WPA2_PSK\n");
break;
case WIFI_AUTH_WPA_WPA2_PSK:
printf("Authmode: WIFI_AUTH_WPA_WPA2_PSK\n");
break;
case WIFI_AUTH_ENTERPRISE:
printf("Authmode: WIFI_AUTH_ENTERPRISE\n");
break;
case WIFI_AUTH_WPA3_PSK:
printf("Authmode: WIFI_AUTH_WPA3_PSK\n");
break;
case WIFI_AUTH_WPA2_WPA3_PSK:
printf("Authmode: WIFI_AUTH_WPA2_WPA3_PSK\n");
break;
case WIFI_AUTH_WPA3_ENTERPRISE:
printf("Authmode: WIFI_AUTH_WPA3_ENTERPRISE\n");
break;
case WIFI_AUTH_WPA2_WPA3_ENTERPRISE:
printf("Authmode: WIFI_AUTH_WPA2_WPA3_ENTERPRISE\n");
break;
case WIFI_AUTH_WPA3_ENT_192:
printf("Authmode: WIFI_AUTH_WPA3_ENT_192\n");
break;
default:
printf("Authmode: WIFI_AUTH_UNKNOWN\n");
break;
}
}
/**
* @brief 打印WIFI密码类型函数
*
* @param pairwise_cipher 密码类型
* @param group_cipher 群密码类型
*/
void print_cipher_type(int pairwise_cipher, int group_cipher)
{
switch (pairwise_cipher)
{
case WIFI_CIPHER_TYPE_NONE:
printf("Pairwise Cipher: WIFI_CIPHER_TYPE_NONE\n");
break;
case WIFI_CIPHER_TYPE_WEP40:
printf("Pairwise Cipher: WIFI_CIPHER_TYPE_WEP40\n");
break;
case WIFI_CIPHER_TYPE_WEP104:
printf("Pairwise Cipher: WIFI_CIPHER_TYPE_WEP104\n");
break;
case WIFI_CIPHER_TYPE_TKIP:
printf("Pairwise Cipher: WIFI_CIPHER_TYPE_TKIP\n");
break;
case WIFI_CIPHER_TYPE_CCMP:
printf("Pairwise Cipher: WIFI_CIPHER_TYPE_CCMP\n");
break;
case WIFI_CIPHER_TYPE_TKIP_CCMP:
printf("Pairwise Cipher: WIFI_CIPHER_TYPE_TKIP_CCMP\n");
break;
case WIFI_CIPHER_TYPE_AES_CMAC128:
printf("Pairwise Cipher: WIFI_CIPHER_TYPE_AES_CMAC128\n");
break;
case WIFI_CIPHER_TYPE_SMS4:
printf("Pairwise Cipher: WIFI_CIPHER_TYPE_SMS4\n");
break;
case WIFI_CIPHER_TYPE_GCMP:
printf("Pairwise Cipher: WIFI_CIPHER_TYPE_GCMP\n");
break;
case WIFI_CIPHER_TYPE_GCMP256:
printf("Pairwise Cipher: WIFI_CIPHER_TYPE_GCMP256\n");
break;
default:
printf("Pairwise Cipher: WIFI_CIPHER_TYPE_UNKNOWN\n");
break;
}
switch (group_cipher)
{
case WIFI_CIPHER_TYPE_NONE:
printf("Group Cipher: WIFI_CIPHER_TYPE_NONE\n");
break;
case WIFI_CIPHER_TYPE_WEP40:
printf("Group Cipher: WIFI_CIPHER_TYPE_WEP40\n");
break;
case WIFI_CIPHER_TYPE_WEP104:
printf("Group Cipher: WIFI_CIPHER_TYPE_WEP104\n");
break;
case WIFI_CIPHER_TYPE_TKIP:
printf("Group Cipher: WIFI_CIPHER_TYPE_TKIP\n");
break;
case WIFI_CIPHER_TYPE_CCMP:
printf("Group Cipher: WIFI_CIPHER_TYPE_CCMP\n");
break;
case WIFI_CIPHER_TYPE_TKIP_CCMP:
printf("Group Cipher: WIFI_CIPHER_TYPE_TKIP_CCMP\n");
break;
case WIFI_CIPHER_TYPE_SMS4:
printf("Group Cipher: WIFI_CIPHER_TYPE_SMS4\n");
break;
case WIFI_CIPHER_TYPE_GCMP:
printf("Group Cipher: WIFI_CIPHER_TYPE_GCMP\n");
break;
case WIFI_CIPHER_TYPE_GCMP256:
printf("Group Cipher: WIFI_CIPHER_TYPE_GCMP256\n");
break;
default:
printf("Group Cipher: WIFI_CIPHER_TYPE_UNKNOWN\n");
break;
}
}
/**
* @brief 扫描WiFi函数
*
*/
void bsp_wifi_scan(void)
{
esp_netif_init(); // 网卡初始化
esp_event_loop_create_default(); // 创建新的事件循环
esp_netif_create_default_wifi_sta(); // 使用默认的设置初始化STA模式
wifi_init_config_t wifi_init_config = WIFI_INIT_CONFIG_DEFAULT();
esp_wifi_init(&wifi_init_config); // WiFi配置初始化
uint16_t number = DEFAULT_SCAN_WIFI_LIST_COUNT; // 默认扫描列表大小
wifi_ap_record_t ap_info[DEFAULT_SCAN_WIFI_LIST_COUNT];
memset(ap_info, 0, sizeof(ap_info));
esp_wifi_set_mode(WIFI_MODE_STA); // 设置WiFi模式为STA模式
esp_wifi_start(); // 启动WiFi
esp_wifi_scan_start(NULL, true); // 开始扫描附近的WiFi
uint16_t ap_count = 0;
esp_wifi_scan_get_ap_num(&ap_count); // 获取上次扫描中找到的AP数量
esp_wifi_scan_get_ap_records(&number, ap_info); // 获取上次扫描中找到的AP列表
printf("Total APs scanned = %u\n\n", ap_count);
// 下面是打印附近的WiFi信息
for (int i = 0; (i < DEFAULT_SCAN_WIFI_LIST_COUNT) && (i < ap_count); i++)
{
printf("SSID: %s\n", ap_info[i].ssid);
printf("RSSI: %d\n", ap_info[i].rssi);
print_auth_mode(ap_info[i].authmode);
if (ap_info[i].authmode != WIFI_AUTH_WEP)
{
print_cipher_type(ap_info[i].pairwise_cipher, ap_info[i].group_cipher);
}
printf("Channel: %d\n\n", ap_info[i].primary);
}
}
然后,我们修改【components】文件夹下【peripheral】文件夹下的 CMakeLists.txt
文件。
# 源文件路径
set(src_dirs src)
# 头文件路径
set(include_dirs inc)
# 设置依赖库
set(requires
driver
esp_wifi
)
# 注册组件到构建系统的函数
idf_component_register(
# 源文件路径
SRC_DIRS ${src_dirs}
# 自定义头文件的路径
INCLUDE_DIRS ${include_dirs}
# 依赖库的路径
REQUIRES ${requires}
)
# 设置特定组件编译选项的函数
# -ffast-math: 允许编译器进行某些可能减少数学运算精度的优化,以提高性能。
# -O3: 这是一个优化级别选项,指示编译器尽可能地进行高级优化以生成更高效的代码。
# -Wno-error=format: 这将编译器关于格式字符串不匹配的警告从错误降级为警告。
# -Wno-format: 这将完全禁用关于格式字符串的警告。
component_compile_options(-ffast-math -O3 -Wno-error=format=-Wno-format)
修改【main】文件夹下的 main.c
文件。
#include "nvs_flash.h"
#include "freertos/FreeRTOS.h"
#include "bsp_wifi.h"
// app_main()函数是ESP32的入口函数,它是FreRTOS的一个任务,任务优先级是1
// main()函数是C语言入口函数,它会在编译过程中插入到二进制文件中的
void app_main(void)
{
esp_err_t result = nvs_flash_init(); // 初始化NVS
if (result == ESP_ERR_NVS_NO_FREE_PAGES || result == ESP_ERR_NVS_NEW_VERSION_FOUND)
{
ESP_ERROR_CHECK(nvs_flash_erase());
ESP_ERROR_CHECK(nvs_flash_init());
}
bsp_wifi_scan();
while (1)
{
// 将一个任务延迟给定的滴答数,IDF中提供pdMS_TO_TICKS可以将指定的ms转换为对应的tick数
vTaskDelay(pdMS_TO_TICKS(100));
}
}
5.2、连接WiFi例程
程序下载成功后,需要利用手机或其他设备创建一个 WiFi 热点。在创建热点时,需要注意提供正确的账号名和密码,以确保程序能够成功连接。同时,确保程序中要连接的热点账号与密码与所创建的热点一致。
我们修改【components】文件夹下的【peripheral】文件夹下的【inc】文件夹下的 bsp_wifi.h
文件。
#ifndef __BSP_WIFI_H__
#define __BSP_WIFI_H__
#include <stdio.h>
#include <string.h>
#include "esp_wifi.h"
void bsp_wifi_sta_init(char *ssid, char *password, wifi_auth_mode_t auth_mode);
#endif // !__BSP_WIFI_H__
我们修改【components】文件夹下的【peripheral】文件夹下的【src】文件夹下的 bsp_wifi.c
文件。
#include "bsp_wifi.h"
static void bsp_wifi_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data);
/**
* @brief 连接WiFi函数
*
* @param ssid WiFi名称
* @param password WiFi密码
* @param auth_mode WiFi认证模式
*/
void bsp_wifi_sta_init(char *ssid, char *password, wifi_auth_mode_t auth_mode)
{
wifi_config_t wifi_config = {
.sta.threshold.authmode = auth_mode, // 认证模式
};
memset(wifi_config.sta.ssid, 0, sizeof(wifi_config.sta.ssid));
memcpy(wifi_config.sta.ssid, ssid, strlen(ssid)); // ssid
memset(wifi_config.sta.password, 0, sizeof(wifi_config.sta.password));
memcpy(wifi_config.sta.password, password, strlen(password)); // 密码
esp_netif_init(); // 网卡初始化
esp_event_loop_create_default(); // 创建新的事件循环
esp_netif_create_default_wifi_sta(); // 使用默认的设置初始化STA模式
wifi_init_config_t wifi_init_config = WIFI_INIT_CONFIG_DEFAULT();
esp_wifi_init(&wifi_init_config); // WiFi配置初始化
esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &bsp_wifi_event_handler, NULL); // 注册WiFi事件处理函数
esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &bsp_wifi_event_handler, NULL); // 注册IP事件处理函数
esp_wifi_set_mode(WIFI_MODE_STA); // 设置WiFi模式为STA模式
esp_wifi_set_config(ESP_IF_WIFI_STA, &wifi_config); // 设置WiFi配置
esp_wifi_start(); // 启动WiFi
}
/**
* @brief WiFi STA模式事件处理函数
*
* @param arg 用户自定义参数,通常为NULL
* @param event_base 事件的基础类型,用于区分不同的事件源(如WiFi事件、IP事件等)
* @param event_id 事件的ID,用于区分同一事件源下的不同事件
* @param event_data 事件相关的数据,具体内容取决于事件类型
*/
static void bsp_wifi_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data)
{
if (event_base == WIFI_EVENT) // WiFi事件
{
switch (event_id)
{
case WIFI_EVENT_STA_START: // WiFi启动STA模式事件
esp_wifi_connect(); // 启动WiFi连接
break;
case WIFI_EVENT_STA_CONNECTED: // WiFi连接成功事件
printf("connect to ap success\n");
break;
case WIFI_EVENT_STA_DISCONNECTED: // WiFi断开连接事件
esp_wifi_connect();
printf("retry to connect to the AP\n");
break;
}
}
else if (event_base == IP_EVENT) // 获取IP事件
{
switch (event_id)
{
case IP_EVENT_STA_GOT_IP: // 获取从路由器分配的IP地址事件
ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data;
printf("get ip:" IPSTR "\n", IP2STR(&event->ip_info.ip));
break;
}
}
}
修改【main】文件夹下的 main.c
文件。
#include "nvs_flash.h"
#include "freertos/FreeRTOS.h"
#include "bsp_wifi.h"
// app_main()函数是ESP32的入口函数,它是FreRTOS的一个任务,任务优先级是1
// main()函数是C语言入口函数,它会在编译过程中插入到二进制文件中的
void app_main(void)
{
esp_err_t result = nvs_flash_init(); // 初始化NVS
if (result == ESP_ERR_NVS_NO_FREE_PAGES || result == ESP_ERR_NVS_NEW_VERSION_FOUND)
{
ESP_ERROR_CHECK(nvs_flash_erase());
ESP_ERROR_CHECK(nvs_flash_init());
}
bsp_wifi_sta_init("HUAWEI-1AA2CE", "12345678", WIFI_AUTH_WPA2_PSK);
while (1)
{
// 将一个任务延迟给定的滴答数,IDF中提供pdMS_TO_TICKS可以将指定的ms转换为对应的tick数
vTaskDelay(pdMS_TO_TICKS(100));
}
}
5.3、WiFi热点例程
程序下载成功后,我们利用手机连接 ESP32 S3 热点设备,当手机连接热点设备成功时,打印连接热点设备的手机的 MAC 地址等信息,当手机从已连接状态断开时,打印断开的外部设备的MAC 地址。
我们修改【components】文件夹下的【peripheral】文件夹下的【inc】文件夹下的 bsp_wifi.h
文件。
#ifndef __BSP_WIFI_H__
#define __BSP_WIFI_H__
#include <stdio.h>
#include <string.h>
#include "esp_wifi.h"
void bsp_wifi_ap_init(char *ssid, char *password, wifi_auth_mode_t auth_mode, uint8_t max_connection);
#endif // !__BSP_WIFI_H__
我们修改【components】文件夹下的【peripheral】文件夹下的【src】文件夹下的 bsp_wifi.c
文件,在该文件中添加如下函数:
/**
* @brief 创建WiFi热点函数
*
* @param ssid WiFi热点名称
* @param password WiFi热点密码
* @param auth_mode WiFi热点认证模式
* @param max_connection WiFi热点最大连接数
*/
void bsp_wifi_ap_init(char *ssid, char *password, wifi_auth_mode_t auth_mode, uint8_t max_connection)
{
esp_netif_init(); // 初始化网卡
esp_event_loop_create_default(); // 创建新的事件循环
esp_netif_create_default_wifi_ap(); // 使用默认配置初始化AP模式
wifi_init_config_t wifi_init_config = WIFI_INIT_CONFIG_DEFAULT();
esp_wifi_init(&wifi_init_config); // WiFi配置初始化
esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &bsp_wifi_event_handler, NULL); // 注册WiFi事件处理函数
// 配置热点信息
wifi_config_t wifi_config = {
.ap = {
.ssid_len = strlen(ssid), // ssid长度
.authmode = auth_mode, // 认证模式
.max_connection = max_connection, // 最大连接数
},
};
if (strlen(password) == 0)
{
wifi_config.ap.authmode = WIFI_AUTH_OPEN;
}
memset(wifi_config.ap.ssid, 0, sizeof(wifi_config.sta.ssid));
memcpy(wifi_config.ap.ssid, ssid, strlen(ssid)); // ssid
memset(wifi_config.ap.password, 0, sizeof(wifi_config.sta.password));
memcpy(wifi_config.ap.password, password, strlen(password)); // 密码
esp_wifi_set_mode(WIFI_MODE_AP); // 设置WiFi模式为AP模式
esp_wifi_set_config(ESP_IF_WIFI_AP, &wifi_config); // 设置WiFi配置
esp_wifi_start(); // 启动WiFi
printf("wifi ap init finished. SSID: %s, password: %s\n", ssid, password);
}
我们修改【components】文件夹下的【peripheral】文件夹下的【src】文件夹下的 bsp_wifi.c
文件,修改该文件的 bsp_wifi_event_handler()
函数:
/**
* @brief WiFi事件处理函数
*
* @param arg 用户自定义参数,通常为NULL
* @param event_base 事件的基础类型,用于区分不同的事件源(如WiFi事件、IP事件等)
* @param event_id 事件的ID,用于区分同一事件源下的不同事件
* @param event_data 事件相关的数据,具体内容取决于事件类型
*/
static void bsp_wifi_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data)
{
if (event_base == WIFI_EVENT) // WiFi事件
{
switch (event_id)
{
case WIFI_EVENT_AP_STACONNECTED: // 设备连接事件
wifi_event_ap_staconnected_t *wifi_ap_connect_event = (wifi_event_ap_staconnected_t *)event_data;
printf("station(%02x:%02x:%02x:%02x:%02x:%02x) join, AID=%d",
wifi_ap_connect_event->mac[0], wifi_ap_connect_event->mac[1], wifi_ap_connect_event->mac[2],
wifi_ap_connect_event->mac[3], wifi_ap_connect_event->mac[4], wifi_ap_connect_event->mac[5],
wifi_ap_connect_event->aid);
break;
case WIFI_EVENT_AP_STADISCONNECTED: // 设备断开事件
wifi_event_ap_stadisconnected_t *wifi_ap_disconnect_event = (wifi_event_ap_stadisconnected_t *)event_data;
printf("station(%02x:%02x:%02x:%02x:%02x:%02x) leave, AID=%d",
wifi_ap_disconnect_event->mac[0], wifi_ap_disconnect_event->mac[1], wifi_ap_disconnect_event->mac[2],
wifi_ap_disconnect_event->mac[3], wifi_ap_disconnect_event->mac[4], wifi_ap_disconnect_event->mac[5],
wifi_ap_disconnect_event->aid);
break;
}
}
}
修改【main】文件夹下的 main.c
文件。
#include "nvs_flash.h"
#include "freertos/FreeRTOS.h"
#include "bsp_wifi.h"
// app_main()函数是ESP32的入口函数,它是FreRTOS的一个任务,任务优先级是1
// main()函数是C语言入口函数,它会在编译过程中插入到二进制文件中的
void app_main(void)
{
esp_err_t result = nvs_flash_init(); // 初始化NVS
if (result == ESP_ERR_NVS_NO_FREE_PAGES || result == ESP_ERR_NVS_NEW_VERSION_FOUND)
{
ESP_ERROR_CHECK(nvs_flash_erase());
ESP_ERROR_CHECK(nvs_flash_init());
}
bsp_wifi_ap_init("Sakura", "12345678", WIFI_AUTH_WPA2_PSK, 8);
while (1)
{
// 将一个任务延迟给定的滴答数,IDF中提供pdMS_TO_TICKS可以将指定的ms转换为对应的tick数
vTaskDelay(pdMS_TO_TICKS(100));
}
}
5.4、WiFi一键配网例程
ESP32 S3的一键配网模式是一种方便快捷的 WiFi 配置方式。在这种模式下,用户无需手动输入 WiFi 的 SSID 和密码等信息,只需要通过一键操作,即可完成 WiFi 的配置和连接。
目前主流的 WiFi 配网方式主要有以下三种:
【1】、SoftAP 配网
ESP32 S3 会建立一个 WiFi 热点(AP 模式),用户将手机连接到这个热点后,将要连接的 WiFi 信息发送给 ESP32 S3,ESP32 S3 得到 SSID 和密码。
这种配网方式很可靠,成功率基本达到 100%,但需要手动切换手机 WiFi 连接的网络,先连接到 ESP32 的 AP 网络,配置完成后再恢复连接正常 WiFi 网络,操作上存在复杂性。
【2】、Smartconfig 配网
ESP32 S3 处于混杂模式下,监听网络中的所有报文,手机 APP 将当前连接的 SSID 和密码编码到 UDP 报文中,通过广播或组播的方式发送报文,ESP32 S3 接收到 UDP 报文后解码,得到 SSID 和密码,然后使用该组 SSID 和密码去连接网络。
这种配网方式简洁,用户容易操作,但配网成功率受环境影响较大。
如果我们要使用 Smartconfig 配网方式,需要从乐鑫官方下载对应的软件,然后在手机上安装使用 https://www.espressif.com.cn/zh-hans/support/download/apps。
【3】、Airkiss 配网
AirKiss 是微信硬件平台提供的一种 WIFI 设备快速入网配置技术。要使用微信客户端的方式配置设备入网,需要设备支持 AirKiss 技术。Airkiss 的原理和 Smartconfig 很类似,设备工作在混杂模式下,微信客户端发送包含 SSID 和密码的广播包,设备收到广播包解码得到 SSID 和密码。
这种配网方式简洁,用户容易操作,但配网成功率同样受环境影响较大。
我们修改【components】文件夹下的【peripheral】文件夹下的【inc】文件夹下的 bsp_wifi.h
文件。
#ifndef __BSP_WIFI_H__
#define __BSP_WIFI_H__
#include <stdio.h>
#include <string.h>
#include "esp_wifi.h"
#include "esp_smartconfig.h"
void bsp_wifi_smartconfig_init(smartconfig_type_t type);
#endif // !__BSP_WIFI_H__
我们修改【components】文件夹下的【peripheral】文件夹下的【src】文件夹下的 bsp_wifi.c
文件,在该文件中添加如下函数:
/**
* @brief SmartConfig一键配网函数
*
* @param type 一键配网方式
*/
void bsp_wifi_smartconfig_init(smartconfig_type_t type)
{
esp_netif_init(); // 初始化网卡
esp_event_loop_create_default(); // 创建新的事件循环
esp_netif_create_default_wifi_sta(); // 用默认的设置初始化STA模式
wifi_init_config_t wifi_init_config = WIFI_INIT_CONFIG_DEFAULT();
esp_wifi_init(&wifi_init_config); // WiFi初始化
esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &bsp_wifi_event_handler, NULL); // 注册WiFi事件处理函数
esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &bsp_wifi_event_handler, NULL); // 注册IP事件处理函数
esp_event_handler_register(SC_EVENT, ESP_EVENT_ANY_ID, &bsp_wifi_event_handler, NULL); // 注册SmartConfig事件处理函数
esp_wifi_set_mode(WIFI_MODE_STA); // 设置WiFi模式为STA模式
esp_wifi_start(); // 启动WiFi
esp_smartconfig_set_type(type); // 设置smartconfig的类型
smartconfig_start_config_t smart_config = SMARTCONFIG_START_CONFIG_DEFAULT();
esp_smartconfig_start(&smart_config);
}
我们修改【components】文件夹下的【peripheral】文件夹下的【src】文件夹下的 bsp_wifi.c
文件,修改该文件的 bsp_wifi_event_handler()
函数:
/**
* @brief WiFi SmartConfig事件处理函数
*
* @param arg 用户自定义参数,通常为NULL
* @param event_base 事件的基础类型,用于区分不同的事件源(如WiFi事件、IP事件等)
* @param event_id 事件的ID,用于区分同一事件源下的不同事件
* @param event_data 事件相关的数据,具体内容取决于事件类型
*/
static void bsp_wifi_event_handler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data)
{
if (event_base == WIFI_EVENT) // WiFi事件
{
switch (event_id)
{
case WIFI_EVENT_STA_START: // WiFi启动STA模式事件
esp_wifi_connect(); // 启动WiFi连接
break;
case WIFI_EVENT_STA_CONNECTED: // WiFi连接成功事件
printf("connect to ap success\n");
break;
case WIFI_EVENT_STA_DISCONNECTED: // WiFi断开连接事件
esp_wifi_connect();
printf("retry to connect to the AP\n");
break;
}
}
else if (event_base == IP_EVENT) // 获取IP事件
{
switch (event_id)
{
case IP_EVENT_STA_GOT_IP: // 获取从路由器分配的IP地址事件
ip_event_got_ip_t *get_ip_envent = (ip_event_got_ip_t *)event_data;
printf("get ip:" IPSTR "\n", IP2STR(&get_ip_envent->ip_info.ip));
break;
}
}
else if (event_base == SC_EVENT) // 获取SmartConfig事件
{
switch (event_id)
{
case SC_EVENT_GOT_SSID_PSWD: // 获取SSID和密码事件
smartconfig_event_got_ssid_pswd_t *smart_config_event = (smartconfig_event_got_ssid_pswd_t *)event_data;
wifi_config_t wifi_config = {
.sta.bssid_set = smart_config_event->bssid_set, // 是否需要设置MAC地址
};
memset(&wifi_config.sta.ssid, 0, sizeof(wifi_config.sta.ssid));
memcpy(wifi_config.sta.ssid, smart_config_event->ssid, sizeof(wifi_config.sta.ssid));
memset(&wifi_config.sta.password, 0, sizeof(wifi_config.sta.password));
memcpy(wifi_config.sta.password, smart_config_event->password, sizeof(wifi_config.sta.password));
// 如果需要设置MAC地址,则拷贝
if (smart_config_event->bssid_set)
{
memset(&wifi_config.sta.bssid, 0, sizeof(wifi_config.sta.bssid));
memcpy(wifi_config.sta.bssid, smart_config_event->bssid, sizeof(wifi_config.sta.bssid));
}
esp_wifi_disconnect(); // 断开WiFi连接
esp_wifi_set_config(WIFI_IF_STA, &wifi_config); // 设置WiFi配置
esp_wifi_connect(); // 重新连接WiFi
break;
case SC_EVENT_SEND_ACK_DONE: // 发送ACK完成事件
esp_smartconfig_stop();
printf("smartconfig done\n");
break;
}
}
}
修改【main】文件夹下的 main.c
文件。
#include "nvs_flash.h"
#include "freertos/FreeRTOS.h"
#include "bsp_wifi.h"
// app_main()函数是ESP32的入口函数,它是FreRTOS的一个任务,任务优先级是1
// main()函数是C语言入口函数,它会在编译过程中插入到二进制文件中的
void app_main(void)
{
esp_err_t result = nvs_flash_init(); // 初始化NVS
if (result == ESP_ERR_NVS_NO_FREE_PAGES || result == ESP_ERR_NVS_NEW_VERSION_FOUND)
{
ESP_ERROR_CHECK(nvs_flash_erase());
ESP_ERROR_CHECK(nvs_flash_init());
}
bsp_wifi_smartconfig_init(SC_TYPE_ESPTOUCH);
while (1)
{
// 将一个任务延迟给定的滴答数,IDF中提供pdMS_TO_TICKS可以将指定的ms转换为对应的tick数
vTaskDelay(pdMS_TO_TICKS(100));
}
}
程序下载成功后,我们打开 EspTouch 软件,在此软件下点击 EspTouch 选项,我们填写好手机连接的 WiFi 密码和传输方式,可按下确定按键发送 UDP 报文。当 ESP32 S3 设备接收到这个报文时,系统会提取该报文的 SSID 和密码去连接该网络。
六、UDP例程
我们在【components】文件夹下的【peripheral】文件夹下的【inc】文件夹(用来存放头文件)新建一个 bsp_udp.h
文件,在【components】文件夹下的【peripheral】文件夹下的【src】文件夹(用来存放源文件)新建一个 bsp_udp.c
文件。
#ifndef __BSP_UDP_H__
#define __BSP_UDP_H__
#include <sys/socket.h>
#include "freertos/task.h"
#define UDP_RECEIVE_BUFF_SIZE 200 // 最大接收数据长度
extern bool g_udp_connect_flag; // UDP连接标志位
extern socklen_t g_udp_socket_fd; // 套接字描述符
extern struct sockaddr_in g_udp_remote_address; // 远端地址
extern uint8_t g_udp_receive_buff[UDP_RECEIVE_BUFF_SIZE]; // 接收数据缓冲区
extern uint16_t g_udp_receive_buff_length; // 接收数据缓冲区大小
void bsp_udp_init(uint16_t port);
#endif // !__BSP_UDP_H__
#include "bsp_udp.h"
bool g_udp_connect_flag = false; // UDP连接标志位
socklen_t g_udp_socket_fd; // 套接字描述符
struct sockaddr_in g_udp_remote_address; // 远端地址
uint8_t g_udp_receive_buff[UDP_RECEIVE_BUFF_SIZE]; // 接收数据缓冲区
uint16_t g_udp_receive_buff_length; // 接收数据缓冲区大小
#define UDP_SEND_DATA_TASK_PRIORITY 1 // 发送数据任务优先级
#define UDP_SEND_DATA_TASK_STACK_SIZE 4096 // 发送数据任务栈大小
TaskHandle_t g_udp_send_data_task_handle; // 发送数据任务句柄
void udp_send_data_task(void *pvParameters); // 发送数据任务函数
#define UDP_RECEIVE_DATA_TASK_PRIORITY 1 // 接收数据任务优先级
#define UDP_RECEIVE_DATA_TASK_STACK_SIZE 4096 // 接收数据任务栈大小
TaskHandle_t g_udp_receive_data_task_handle; // 接收数据任务句柄
void udp_receive_data_task(void *pvParameters); // 接收数据任务函数
/**
* @brief UDP初始化函数
*
* @param port 本地端口号
*
* @note 该函数会创建一个UDP套接字,并绑定到指定的本地端口上。
* 同时创建两个任务,一个用于接收数据,一个用于发送数据。
*/
void bsp_udp_init(uint16_t port)
{
int result = 0;
struct sockaddr_in udp_address = {
.sin_addr.s_addr = htons(INADDR_ANY), // 设置本地IP地址
.sin_port = htons(port), // 设置端口号
.sin_family = AF_INET, // IPv4地址
};
g_udp_socket_fd = socket(AF_INET, SOCK_DGRAM, 0); // 创建UDP套接字
// 绑定本地的地址信息到套接字中
result = bind(g_udp_socket_fd, (struct sockaddr *)&udp_address, sizeof(udp_address));
if (result)
{
printf("udp bind socket failed\n");
closesocket(g_udp_socket_fd); // 关闭套接字
}
else
{
printf("udp bind socket success\n");
// 创建发送数据任务
xTaskCreate(
udp_send_data_task, // 任务函数
"udp_send_data_task", // 任务名
UDP_SEND_DATA_TASK_STACK_SIZE, // 任务栈大小
NULL, // 任务参数
UDP_SEND_DATA_TASK_PRIORITY, // 任务优先级
&g_udp_send_data_task_handle // 任务句柄
);
// 创建接收数据任务
xTaskCreate(
udp_receive_data_task, // 任务函数
"udp_receive_data_task", // 任务名
UDP_RECEIVE_DATA_TASK_STACK_SIZE, // 任务栈大小
NULL, // 任务参数
UDP_RECEIVE_DATA_TASK_PRIORITY, // 任务优先级
&g_udp_receive_data_task_handle // 任务句柄
);
}
}
/**
* @brief UDP发送数据任务函数
*
* @param pvParameters 任务参数
*/
void udp_send_data_task(void *pvParameters)
{
uint32_t result = 0;
while (1)
{
result = ulTaskNotifyTake(pdTRUE, portMAX_DELAY); // 接收任务通知
if (result && g_udp_receive_buff_length)
{
sendto(
g_udp_socket_fd, // scoket
g_udp_receive_buff, // 发送数据缓冲区
strlen((char *)g_udp_receive_buff), // 发送的数据大小
0, // 数据发送时的特殊处理
(struct sockaddr *)&g_udp_remote_address, // 接收端地址信息
sizeof(g_udp_remote_address) // 接收端地址信息大小
); // 发送数据
memset(g_udp_receive_buff, 0, sizeof(g_udp_receive_buff));
g_udp_receive_buff_length = 0;
}
}
}
/**
* @brief UDP接收数据任务函数
*
* @param pvParameters 任务参数
*/
void udp_receive_data_task(void *pvParameters)
{
// 如果要获取发送端的地址信息,则该参数的值是一个sockaddr结构体的大小,如果不需要,则该参数的值是0
socklen_t from_length = sizeof(g_udp_remote_address);
while (1)
{
g_udp_receive_buff_length = recvfrom(
g_udp_socket_fd, // socket
(void *)g_udp_receive_buff, // 接收数据缓冲区
sizeof(g_udp_receive_buff), // 接收数据缓冲区大小
0, // 用户控制接收的方式
(struct sockaddr *)&g_udp_remote_address, // 保存发送端的地址信息
&from_length // 保存接收端地址信息大小
);
if (g_udp_receive_buff_length)
{
printf("%s\r\n",g_udp_receive_buff);
xTaskNotifyGive(g_udp_send_data_task_handle); // 发送任务通知
}
}
}
我们修改【components】文件夹下的【peripheral】文件夹下的【src】文件夹下的 bsp_wifi.c
文件,修改该文件的 bsp_wifi_event_handler()
函数,在该函数中添加一个标志,用来表示是否连接 WiFi 成功:
/**
* @brief WiFi事件处理函数
*
* @param arg 用户自定义参数,通常为NULL
* @param event_base 事件的基础类型,用于区分不同的事件源(如WiFi事件、IP事件等)
* @param event_id 事件的ID,用于区分同一事件源下的不同事件
* @param event_data 事件相关的数据,具体内容取决于事件类型
*/
static void bsp_wifi_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data)
{
if (event_base == WIFI_EVENT) // WiFi事件
{
switch (event_id)
{
case WIFI_EVENT_STA_START: // WiFi启动STA模式事件
g_wifi_connect_flag = false;
esp_wifi_connect(); // 启动WiFi连接
break;
case WIFI_EVENT_STA_CONNECTED: // WiFi连接成功事件
// g_wifi_connect_flag = true;
printf("connect to ap success\n");
break;
case WIFI_EVENT_STA_DISCONNECTED: // WiFi断开连接事件
g_wifi_connect_flag = false;
esp_wifi_connect();
printf("retry to connect to the AP\n");
break;
}
}
else if (event_base == IP_EVENT) // 获取IP事件
{
switch (event_id)
{
case IP_EVENT_STA_GOT_IP: // 获取从路由器分配的IP地址事件
ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data;
printf("get ip:" IPSTR "\n", IP2STR(&event->ip_info.ip));
g_wifi_connect_flag = true;
break;
}
}
}
修改【main】文件夹下的 main.c
文件。
#include "nvs_flash.h"
#include "freertos/FreeRTOS.h"
#include "bsp_wifi.h"
#include "bsp_udp.h"
// app_main()函数是ESP32的入口函数,它是FreRTOS的一个任务,任务优先级是1
// main()函数是C语言入口函数,它会在编译过程中插入到二进制文件中的
void app_main(void)
{
esp_err_t result = nvs_flash_init(); // 初始化NVS
if (result == ESP_ERR_NVS_NO_FREE_PAGES || result == ESP_ERR_NVS_NEW_VERSION_FOUND)
{
ESP_ERROR_CHECK(nvs_flash_erase());
ESP_ERROR_CHECK(nvs_flash_init());
}
bsp_wifi_sta_init("HUAWEI-1AA2CE", "12345678", WIFI_AUTH_WPA2_PSK);
while (1)
{
while (!g_udp_connect_flag)
{
if (g_wifi_connect_flag)
{
bsp_udp_init(8000);
g_udp_connect_flag = true;
}
vTaskDelay(pdMS_TO_TICKS(1000));
}
// 将一个任务延迟给定的滴答数,IDF中提供pdMS_TO_TICKS可以将指定的ms转换为对应的tick数
vTaskDelay(pdMS_TO_TICKS(100));
}
}
七、TCP例程
7.1、TCP客户端例程
我们在【components】文件夹下的【peripheral】文件夹下的【inc】文件夹(用来存放头文件)新建一个 bsp_tcp.h
文件,在【components】文件夹下的【peripheral】文件夹下的【src】文件夹(用来存放源文件)新建一个 bsp_tcp.c
文件。
#ifndef __BSP_TCP_H__
#define __BSP_TCP_H__
#include <sys/socket.h>
#include "freertos/task.h"
#define TCP_RECEIVE_BUFF_SIZE 200 // 最大接收数据长度
extern bool g_tcp_connect_flag; // TCP连接标志位
extern socklen_t g_tcp_socket_fd; // 套接字描述符
extern uint8_t g_tcp_receive_buff[TCP_RECEIVE_BUFF_SIZE]; // 接收数据缓冲区
extern uint16_t g_tcp_receive_buff_length; // 接收数据长度
void bsp_tcp_client_init(uint16_t port, char *server_ip, uint16_t server_port);
#endif // !__BSP_TCP_H__
#include "bsp_tcp.h"
bool g_tcp_connect_flag = false; // TCP连接标志位
socklen_t g_tcp_socket_fd; // 套接字描述符
uint8_t g_tcp_receive_buff[TCP_RECEIVE_BUFF_SIZE]; // 接收数据缓冲区
uint16_t g_tcp_receive_buff_length; // 接收数据长度
#define TCP_SEND_DATA_TASK_PRIORITY 1 // 发送数据任务优先级
#define TCP_SEND_DATA_TASK_STACK_SIZE 4096 // 发送数据任务栈大小
TaskHandle_t g_tcp_send_data_task_handle; // 发送数据任务句柄
void tcp_send_data_task(void *pvParameters); // 发送数据任务函数
#define TCP_RECEIVE_DATA_TASK_PRIORITY 1 // 接收数据任务优先级
#define TCP_RECEIVE_DATA_TASK_STACK_SIZE 4096 // 接收数据任务栈大小
TaskHandle_t g_tcp_receive_data_task_handle; // 接收数据任务句柄
void tcp_receive_data_task(void *pvParameters); // 接收数据任务函数
/**
* @brief TCP客户端初始化函数
*
* @param port 客户端端口号
* @param server_ip 服务端IP地址
* @param server_port 服务端端口号
*
* @note 该函数会创建一个TCP套接字,并绑定到指定的客户端端口上。
* 同时创建两个任务,一个用于接收数据,一个用于发送数据。
*/
void bsp_tcp_client_init(uint16_t port, char *server_ip, uint16_t server_port)
{
int result = 0;
struct sockaddr_in tcp_client_address =
{
.sin_addr.s_addr = htonl(INADDR_ANY), // 客户端IP地址
.sin_port = htons(port), // 客户端端口号
.sin_family = AF_INET // IPv4地址
};
struct sockaddr_in tcp_server_address =
{
.sin_addr.s_addr = inet_addr(server_ip), // 服务端IP地址
.sin_port = htons(server_port), // 服务端端口号
.sin_family = AF_INET // IPv4地址
};
g_tcp_socket_fd = socket(AF_INET, SOCK_STREAM, 0); // 创建TCP套接字
// 绑定本地的地址信息到套接字中
bind(g_tcp_socket_fd, (struct sockaddr *)&tcp_client_address, sizeof(tcp_client_address));
// 连接服务器
result = connect(g_tcp_socket_fd, (struct sockaddr *)&tcp_server_address, sizeof(tcp_server_address));
if (result)
{
printf("connect server failed\n");
closesocket(g_tcp_socket_fd); // 关闭套接字
}
else
{
printf("connect server success\n");
// 创建发送数据任务
xTaskCreate(
tcp_send_data_task, // 任务函数
"tcp_send_data_task", // 任务名
TCP_SEND_DATA_TASK_STACK_SIZE, // 任务栈大小
NULL, // 任务参数
TCP_SEND_DATA_TASK_PRIORITY, // 任务优先级
&g_tcp_send_data_task_handle // 任务句柄
);
// 创建接收数据任务
xTaskCreate(
tcp_receive_data_task, // 任务函数
"tcp_receive_data_task", // 任务名
TCP_RECEIVE_DATA_TASK_STACK_SIZE, // 任务栈大小
NULL, // 任务参数
TCP_RECEIVE_DATA_TASK_PRIORITY, // 任务优先级
&g_tcp_receive_data_task_handle // 任务句柄
);
}
}
/**
* @brief TCP发送数据任务函数
*
* @param pvParameters 任务参数
*/
void tcp_send_data_task(void *pvParameters)
{
uint32_t result = 0;
while (1)
{
result = ulTaskNotifyTake(pdTRUE, portMAX_DELAY); // 接收任务通知
if (result && g_tcp_receive_buff_length)
{
send(
g_tcp_socket_fd, // scoket
g_tcp_receive_buff, // 发送数据缓冲区
strlen((char *)g_tcp_receive_buff), // 发送的数据大小
0 // 数据发送时的特殊处理
);
memset(g_tcp_receive_buff, 0, sizeof(g_tcp_receive_buff));
}
}
}
/**
* @brief TCP接收数据任务函数
*
* @param pvParameters 任务参数
*/
void tcp_receive_data_task(void *pvParameters)
{
while (1)
{
g_tcp_receive_buff_length = recv(
g_tcp_socket_fd, // socket
(void *)g_tcp_receive_buff, // 接收数据缓冲区
sizeof(g_tcp_receive_buff), // 接收数据缓冲区大小
0 // 用户控制接收的方式
);
if (g_tcp_receive_buff_length)
{
printf("%s\r\n",g_tcp_receive_buff);
xTaskNotifyGive(g_tcp_send_data_task_handle); // 发送任务通知
}
}
}
修改【main】文件夹下的 main.c
文件。
#include "nvs_flash.h"
#include "freertos/FreeRTOS.h"
#include "bsp_wifi.h"
#include "bsp_tcp.h"
// app_main()函数是ESP32的入口函数,它是FreRTOS的一个任务,任务优先级是1
// main()函数是C语言入口函数,它会在编译过程中插入到二进制文件中的
void app_main(void)
{
esp_err_t result = nvs_flash_init(); // 初始化NVS
if (result == ESP_ERR_NVS_NO_FREE_PAGES || result == ESP_ERR_NVS_NEW_VERSION_FOUND)
{
ESP_ERROR_CHECK(nvs_flash_erase());
ESP_ERROR_CHECK(nvs_flash_init());
}
bsp_wifi_sta_init("HUAWEI-1AA2CE", "12345678", WIFI_AUTH_WPA2_PSK);
while (1)
{
while (!g_tcp_connect_flag)
{
if (g_wifi_connect_flag)
{
bsp_tcp_client_init(8000, "192.168.3.159", 8080);
g_tcp_connect_flag= true;
}
vTaskDelay(pdMS_TO_TICKS(1000));
}
// 将一个任务延迟给定的滴答数,IDF中提供pdMS_TO_TICKS可以将指定的ms转换为对应的tick数
vTaskDelay(pdMS_TO_TICKS(100));
}
}
7.2、TCP服务端例程
我们修改【components】文件夹下的【peripheral】文件夹下的【src】文件夹下的 bsp_tcp.c
文件,在该文件中添加如下函数:
/**
* @brief TCP服务端初始化函数
*
* @param port 服务端端口号
* @param max_connect_count 最大连接数
*
* @note 每一个客户端连接服务端时会创建两个任务,一个用于接收数据,一个用于发送数据。
*/
void bsp_tcp_server_init(uint16_t port, int max_connect_count)
{
int result = 0;
g_semphore_handle = xSemaphoreCreateCounting(30, 0); // 创建计数型信号量
struct sockaddr_in tcp_server_address =
{
.sin_addr.s_addr = htonl(INADDR_ANY), // 客户端IP地址
.sin_port = htons(port), // 客户端端口号
.sin_family = AF_INET // IPv4地址
};
g_tcp_socket_fd = socket(AF_INET, SOCK_STREAM, 0); // 创建TCP套接字
// 绑定本地的地址信息到套接字中
bind(g_tcp_socket_fd, (struct sockaddr *)&tcp_server_address, sizeof(tcp_server_address));
// 开始监听
result = listen(g_tcp_socket_fd, max_connect_count); // 监听套接字
if (result)
{
printf("server listen failed\n");
closesocket(g_tcp_socket_fd); // 关闭套接字
}
else
{
printf("server listen success\n");
while (1)
{
struct sockaddr_in tcp_client_address = {0};
socklen_t client_address_length = sizeof(tcp_client_address); // 客户端地址长度
// 等待客户端连接
socklen_t new_tcp_fd = accept(g_tcp_socket_fd, (struct sockaddr *)&tcp_client_address, &client_address_length);
if (new_tcp_fd != -1)
{
// 获取客户端 IP 地址
char client_str[128] = {0};
char client_send_task_name[128] = {0};
char client_receive_task_name[128] = {0};
char client_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &tcp_client_address.sin_addr, client_ip, sizeof(client_ip));
sprintf(client_str, "client(%s:%d)", client_ip, tcp_client_address.sin_port);
sprintf(client_send_task_name, "%s send data task", client_str);
sprintf(client_receive_task_name, "%s receive data task", client_str);
printf("%s connect success\n", client_str);
// 每连接一个客户端就创建一个发送给客户端的任务
xTaskCreate(
tcp_send_data_task, // 任务函数
client_send_task_name, // 任务名
TCP_SEND_DATA_TASK_STACK_SIZE, // 任务栈大小
(void *)new_tcp_fd, // 任务参数
TCP_SEND_DATA_TASK_PRIORITY, // 任务优先级
NULL // 任务句柄
);
xTaskCreate(
tcp_receive_data_task, // 任务函数
client_receive_task_name, // 任务名
TCP_RECEIVE_DATA_TASK_STACK_SIZE, // 任务栈大小
(void *)new_tcp_fd, // 任务参数
TCP_RECEIVE_DATA_TASK_PRIORITY, // 任务优先级
NULL // 任务句柄
);
}
}
}
}
修改【main】文件夹下的 main.c
文件。
#include "nvs_flash.h"
#include "freertos/FreeRTOS.h"
#include "bsp_wifi.h"
#include "bsp_tcp.h"
// app_main()函数是ESP32的入口函数,它是FreRTOS的一个任务,任务优先级是1
// main()函数是C语言入口函数,它会在编译过程中插入到二进制文件中的
void app_main(void)
{
esp_err_t result = nvs_flash_init(); // 初始化NVS
if (result == ESP_ERR_NVS_NO_FREE_PAGES || result == ESP_ERR_NVS_NEW_VERSION_FOUND)
{
ESP_ERROR_CHECK(nvs_flash_erase());
ESP_ERROR_CHECK(nvs_flash_init());
}
bsp_wifi_sta_init("HUAWEI-1AA2CE", "12345678", WIFI_AUTH_WPA2_PSK);
while (1)
{
while (!g_tcp_connect_flag)
{
if (g_wifi_connect_flag)
{
bsp_tcp_server_init(8080, 5);
g_tcp_connect_flag= true;
}
vTaskDelay(pdMS_TO_TICKS(1000));
}
// 将一个任务延迟给定的滴答数,IDF中提供pdMS_TO_TICKS可以将指定的ms转换为对应的tick数
vTaskDelay(pdMS_TO_TICKS(100));
}
}