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),它负责在主机之间发送和接收数据包。
  • 网络接口层:这是最底层,负责将数据转换为可以在物理媒介上发送的信号。这一层的协议涉及到如何将数据帧封装在数据链路层,以便在网络上进行传输。

TCPIP模型

  每一层都使用下一层提供的服务,同时对上一层提供服务。这种分层结构使得协议栈更加灵活,易于扩展和维护。不同层次上的协议一起工作,协调数据在计算机网络中的传输,使得不同的计算机能够相互通信。

  TCP/IP 协议栈和传统的 OSI 模型并不完全对应。TCP/IP 协议栈是一个简化的模型,强调了实际的协议实现和因特网的实际运作方式。相比之下,OSI 模型更加全面和理想化,它提供了一个框架来描述不同系统之间的交互方式。

TCPIP协议栈的分层结构

  ISO/OSI 分层模型也是一个分层结构,包括七个层次,从上到下分别是:应用层表示层会话层传输层网络层数据链路层物理层。虽然 ISO/OSI 模型为不同的系统之间的通信提供了一个理论框架,但 TCP/IP 协议栈更侧重于实际的协议实现和因特网的实际运作方式。网络技术的发展并不是遵循严格的 ISO/OSI 分层概念。实际上现在的互联网使用的是 TCP/IP 体系结构中某些应用程序可以直接使用 IP 层,或甚至直接使用最下面的网络接口层。

TCPIP体系结构另一种表示方法

  无论那种表示方法,TCP/IP 模型各个层次都分别对应于不同的协议。TCP/IP 协议栈负责确保网络设备之间能够通信。它是一组规则,规定了信息如何在网络中传输。其中,这些协议都分布在应用层,传输层和网络层,网络接口层是由硬件来实现。如 Windows 操作系统包含了 CBISC 协议栈,该协议栈就是实现了 TCP/IP 协议栈的应用层,传输层和网络层的功能,网络接口层由网卡实现,所以 CBISC 协议栈和网卡构建了网络通信的核心骨架。因此,无论哪一款以太网产品,都必须符合 TCP/IP 体系结构,才能实现网络通信。注意:路由器和交换机等相关网络设备只实现网络层和网络接口层的功能。

1.2、TCP/IP协议栈的封包和拆包

  TCP/IP 协议栈的封包和拆包是指在网络通信中,将数据按照一定的协议和格式进行封装和解析的过程。

  在 TCP/IP 协议栈中,数据封 是指在发送端将数据按照协议规定的格式进行打包,以便在网络中进行传输。在应用层的数据被封装后,会经过传输层、网络层和网络接口层的处理,最终转换成可以在物理网络上传输的帧格式。数据封装的过程涉及到对数据的分段、压缩、加密等操作,以确保数据能够可靠、安全地传输到目的地,下图描述的是封包处理流程。

TCPIP协议栈的封包

  数据拆包 是指接收端收到数据后,按照协议规定的格式对数据进行解析和处理,还原出原始的数据。在接收到数据后,接收端会按照协议规定的层次从下往上逐层处理数据,最终将应用层的数据还原出来。数据拆包的过程涉及到对数据的重组、解压缩、解密等操作,以确保数据能够被正确地解析和处理,下图描述的是拆包处理流程。

TCPIP协议栈的拆包

  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 都能提供强大的网络支持。

LwIP基本特性

2.2、LwIP与TCP/IP体系结构的对应关系

LwIP的结构框图

LwIP与TCPIP体系结构的对应图解

  从上图可以清晰地看到,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网络层次示意图

  从上图可以看出,ESP32 S3 芯片内置 WiFi MAC 内核。当我们发送数据到网络时,数据首先被转化为无线信号,然后发送到该设备连接的 WiFi 路由器中。接着,路由器通过网线将数据传输到目标主机,从而完成数据传输操作。

  1. 数据转化为无线信号:当 ESP32 S3 想要发送数据到网络时,它首先会将数据封装到一个无线传输帧中。这一过程涉及到将数据转化为可以在无线介质上传输的格式。
  2. 发送到 WiFi 路由器:封装后的无线信号然后被发送到 ESP32 S3 连接的 WiFi 路由器。WiFi 路由器充当一个中间设备,负责将无线信号转换为有线网络信号(如果目标主机是通过有线网络连接的)或直接转发无线信号(如果目标主机也是通过 WiFi 连接的)。
  3. 路由器传输数据:WiFi 路由器接收到无线信号后,会进一步处理它。如果目标主机是通过有线网络连接的,路由器会将无线信号转换为有线网络信号,并通过网线将其传输到目标主机。如果目标主机也是通过 WiFi 连接的,路由器会直接转发无线信号到目标主机。
  4. 完成数据传输:最终,目标主机接收到路由器发送的有线网络信号或无线信号,并将其解析为原始数据。这样,整个数据传输过程就完成了。

  在整个过程中,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

下载ESP-TOUCH软件

【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 和密码去连接该网络。

SmartConfig演示视频

六、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));
    }
}
posted @ 2025-04-07 22:56  星光映梦  阅读(250)  评论(0)    收藏  举报