S2-10 ESP-IDF开发 : Wi-Fi

发布时间:2024年01月23日

Wi-Fi

之前的课程中,我们了解了一部分常用的单机通讯协议,也就是在一块PCB上,多个MCU或设备之间的互联互通,这些内容基本上能满足我们日常项目80%的需求,也足够我们折腾了。
但是,仅仅会了单机编程,那也仅仅相当于在家折腾,一个人的生活总是孤独寂寞冷的,是时候出去找个伴了……

基础知识

物联通通讯中,想达到设备间的互联互通,有很多中协议可用,具体可分为无线通讯协议和物联网通讯协议两个层面,其中常用的无线通讯协议有:

  1. Wi-Fi: 是一种基于 IEEE 802.11 标准的无线局域网协议,可以提供高速的无线数据传输和广泛的覆盖范围,适用于家庭、企业等场所的局域网环境。
  2. Bluetooth: 是一种短距离无线通信协议,主要应用于移动设备(如手机、平板电脑、耳机等)之间的数据传输和音频传输,具有功耗低、成本低等特点。
  3. ZigBee: 是一种低功耗、短距离无线通信协议,主要应用于智能家居领域,具有节点众多、带宽较窄等特点,通信距离一般在 10 米以内。
  4. Z-Wave: 是一种针对智能家居领域的专用无线通信协议,具有低功耗、节点众多、通信距离远等特点,可以实现智能家居设备之间的互联互通。
  5. LoRaWAN: 是一种基于 LoRa 技术的长距离、低功耗无线通信协议,具有远距离传输、低功耗等特点,适用于大规模物联网应用场景。
  6. NB-IoT:是一种窄带物联网通信技术,可以在现有的 4G 网络上实现低功耗、低速率的物联网通信,适用于需要长距离传输和低功耗的物联网应用场景。

无线通讯协议是用来达到设备间互联互通的,而连接之后,在无线通讯协议(或有线协议)基础上,需要挂载物联网通讯协议,常用的物联网通讯协议有:

  1. MQTT: 是一种基于发布-订阅模式的轻量级消息传输协议,具有低带宽、低功耗、可靠性强等特点,广泛应用于物联网领域。
  2. CoAP: 是基于 REST 架构的轻量级应用层协议,专门针对无线网络和低带宽传输优化而设计,具有灵活性高、安全性好等特点,广泛应用于物联网设备之间的通信。
  3. HTTP: 是一种应用层协议,常用于客户端和服务器之间的请求-响应模式通信,具有通用性好、可扩展性高等特点,在物联网环境中的应用场景比较广泛。
  4. DDS: 是一种数据分发服务协议,可以实现高效的实时数据传输和处理,适用于大规模、复杂、实时交互的物联网应用场景。
  5. ZigBee: 是一种低功耗、短距离无线通信协议,主要应用于智能家居领域,具有低功耗、成本低等特点,通信距离一般在 10 米以内。
  6. LoRaWAN: 是一种基于 LoRa 技术的长距离、低功耗无线通信协议,具有远距离传输、低功耗等特点,适用于大规模物联网应用场景。

以上是几种常见的物联网通讯协议,在实际应用中,需要根据具体的应用场景和需求来选择合适的通讯协议。同时,不同的协议之间也可以进行组合和协同配合,以满足更加复杂和多样化的应用需求。

而今天,我们从Wi-Fi协议开始讲起……

什么是 Wi-Fi

Wi-Fi 是一个无线通讯计数的品牌,由 Wi-Fi 联盟(Wi-Fi Alliance, WFA)拥有。WFA 专门负责 Wi-Fi 认证与商标授权工作,严谨的说,Wi-Fi 是一个认证的名称,该认证用于测试无线网络设备是否符合 IEEE 802.11 系列协议的规范。通过该认证的设备将被授予一个名为 Wi-Fi CERTIFICATE 的商标。相比与其他无线通讯计数,Wi-Fi 具有覆盖广、穿墙性能佳、吞吐量大的优势。不过,随着获得 Wi-Fi 认证的设备普及,人们业就习以为常的称之为 Wi-Fi 无线网络为 Wi-Fi 网络了。

IEEE 802.11 的发展历程

上文书提到 IEEE802.11,这又是个什么东西呢?
IEEE(Institute of Electrical and Electronics Engineers)是美国电气和电子工程协会的英文简称,802 是该协会中的一个专门负责制定局域网标准的委员会,也称为LMSC(LAN/MANStandards Committee,局域网/城域网标准委员会)。由于工作量巨大,该委员会被细分成多个工作组,每个工作组负责解决某个特定方面的问题。工作组会被赋予一个编号(位于802的后面,中间用点号隔开),因此802.11代表802的第11个工作组,专门负责制定无线局域网(Wireless LAN)的媒介访问控制(Medium Access Control,MAC)协议和物理层(PhysicalLayer,PHY)技术规范。IEEE802.11的发展历经好几个版本。

在这里插入图片描述

  • MIMO: Multiple Input Multiple Output,多输入多输出。
  • OFDM: Orthogonal Frequency Division Multiplexing,正交频分复用。
  • CCK: Complementary Code Keying,补码键控。
  • DSSS: Direct Sequence Spread Spectrum,直接序列扩频。
  • OFDMA: Orthogonal Frequency Division Multiple Access,正交频分多址。
Wi-Fi 相关术语
1. OSI/RM

在OSI/RM中,计算机网络体系被划分为七层,其名称和对应关系如下图所示。在这里插入图片描述

2. IEEE 802.11 规范中的物理组件

IEEE 802.11 规范中的物理组件主要范围四种:

  1. 无线媒介(Wireless Medium,WM): 是指能传输无线 MAC 数据的物理层。IEEE 802.11规范最早定义了不止一种物理层,如射频和红外两种物理层,事后证明射频物理层较受欢迎。
  2. 工作站(Station,STA): 所谓的 STA,是指携带无线网络接口卡的设备。通常,STA是以电池供电的膝上型(Laptop)或手持式 (Handheld) 计算机。然而,STA 不见得就是携带型(Portable)计算设备,有时候,使用无线网络的目的是节省布线的麻烦,桌上型(Desktop)计算机一样可以使用无线局域网络。
  3. 无线接入点 (Access Point,AP): AP本身也是一个STA,只不过它还能为那些已经关联的STA 提供分布式服务。
  4. 分布式系统(Distribution System,DS): 当几个AP 串联以覆盖较大区域时,彼此之间必须相互通信才能够掌握移动式STA 的行踪,分布式系统负责将 (Frame) 转送到目的地。
3. 物联网的构建

有了上面描述的物理组件,就可以搭建由这些物理组件组成的无线网络了。在 IEEE 802.11规范中,基本服务集(Basic Service Set,BSS)是整个无线网络的基本构建组件(Basic BuildingBlock)。BSS 有两种类型,即独立型 BSS 和基础结构型 BSS,如下图所示。
在这里插入图片描述

  1. 独立型 BSS(Independent BSS): 该类型的 BSS 不需要 AP 参与,各STA之间可直接交互。

  2. 基础结构型 BSS (Infrastructure BSS): 所有 STA之间的交互必须经过AP,AP是基础结构型 BSS 的中控台。这也是读者最常见的网络架构。在这种网络架构中,一个 STA 必须完成诸如关联、授权等步骤后才能加入某个 BSS。

上述网络架构中都有所谓的Identification,它们分别是:

  1. BSSID(BSS Identification): 每一个BSS 都有用于识别的物理地址,称为 BSSID。在基础结构型 BSS 中,BSSID 就是AP的MAC 地址,该MAC 地址是真实的地址。MAC 地址在设备出厂时会有一个默认值,可更改,也有其固定的命名格式。

  2. SSID(Service Set Identification): 每个AP 都有一个用于用户识别的标识,一般而言BSSID 会和一个SSID 关联。BSSID 是AP的MAC 地址,而 SSID 是用于识别用户的标识其往往是一个可读字符串,也就是我们经常说的 Wi-Fi 名称。

4. Wi-Fi 的连接过程

STA 首先需要通过主动/被动扫描发现周围的无线网络,再通过认证和关联两个过程后,和AP建立连接,最终接入无线局域网。Wi-Fi连接的过程如下图 所示。

STA AP 通过扫描选择 AP 认证 关联 和建立关联的 AP 通信 STA AP

1. 扫描
STA 可以通过两种扫描方式来获取周围的无线网络:

  1. 被动扫描(Passive Sanning: )STA可以通过监听周围AP定期发送的 Beacon(信标帧)发现周围的无线网络。当用户需要节省电量时,推荐使用被动扫描。

  2. 主动扫描(Active Scanning): 主动发送一个探测请求(Probe Request)帧,接收AP返回的探测响应帧(Probe Response)。根据探测请求帧是否携带 SSID,主动扫描可以分为两种:

    1. 不携带SSID的主动扫描: STA 会定期在其支持的信道列表中,发送探测请求来扫描周围的无线网络。当AP 收到探测请求后,会回应探测响应帧,以便通告可以提供的无线网络通过这种方式,STA 可以主动获取周围可使用的无线网络。

    2. 携带指定 SSID的主动扫描: 当 STA 需要配置待连接的无线网络或者已经成功连接到一个无线网络时,STA 会定期发送探测请求帧(该携带了配置信息或者已经连接的无线网络SSID),当能够提供指定 SSID 无线网络的AP 接收到探测请求后回应探测响应帧。通过这种方式,STA 可以主动扫描指定的无线网络。

对于隐藏AP,需要使用携带指定SSID的主动扫描方式。

2. 认证
当STA 找到可使用的无线网络时,在SSID 匹配的 AP 中,可依据连接策略(如信号最优或MAC 地址匹配等)选择合适的 AP,进入认证阶段。认证包括开放式认证和非开放式认证。

  1. 开放式认证: 开放式认证在实质上是完全不认证,也不加密,任何人都可以连接并使用无线网络。当连接到无线网络时,AP 并没有验证 STA 的真实身份。开放式认证的步骤如下图所示。
STA AP 认证请求 认证响应(成功) STA AP

STA 发起认证请求,AP 应答认证结果,如果返回的是成功,则表示两者认证成功。

  1. 非开放式认证: 非开放式认证包括共享密钥、WPA (Wi-Fi Protected Acess,Wi-Fi 保护访问)和RSN(Robust Security Network,强健安全网络)等方式。
    1. 共享密钥(Shared Key): 共享密钥认证依赖于 WEP (Wired Equivalent Privacy,有线等效加密)机制,是最基本的加密技术,其加密的安全性很脆弱。
      STA与AP必须拥有相同的密,才能解读互相传输的数据。密分为64 bit 密及128 bit密钥两种,最多可设定四组不同的密钥。共享密钥认证的步骤下图所示。
STA AP 认证请求 明文 用密文加密铭文 密文 秘钥解密和明文比较 认证响应(成功) STA AP

STA 发起认证请求,AP 收到请求后回复质询文本,STA利用预置密钥将加密后的明文发送给AP,AP 用预置密钥解密明文,并和之前的明文比较,如一致则表示通过认证。

  1. WPA: WPA 是在IEEE 802.1li规范正式发布前用于替代WEP 的一个中间产物,它采用了新的MIC(Message Integrity Check,消息完整性校验)算法,用于替代 WEP 中的 CRC 算法:采用TKIP (TemporalKey Integrity Protocol,临时密钥完整性协议)来为每一个MAC帧生成不同的 Key。TKIP 是一种过渡性的加密协议,现已被证明其安全性不高。

  2. RSN: RSN 被 WFA 称为WPA2,它采用了全新的加密方式 CCMP(Counter Mode withCBC-MAC Protocol,计数器模块及密码块链消息认证码协议),这是一种基于AES (AdvancedEncryption Standard,高级加密标准)的块安全协议,本书后文将结合身份验证详细介绍相关的内容。

  3. WPA3: 虽然 WPA2在一定程度上保证了 Wi-Fi 网络的安全,但 WPA2在应用过程中也不断暴露出很多安全漏洞,如离线字典或暴力破解攻击、KRACK (Key Reinstallation Attacks,密钥重装攻击)等。WFA 于2018 年发布的新一代 Wi-Fi 加密协议 WPA3,改进了 WPA2中存在的安全风险,增加了许多新的功能,为 Wi-Fi 网络的安全性提供了更强的保护。相比WPA2,WPA3的优势如下:

    • 禁止使用过时的 TKIP,强制使用AES 加密算法。
    • 必须对管理帧进行保护。
    • 使用更加安全的SAE(Simultaneous Authentication ofEquals,对等实体同时验证)来取代WPA2中的 PSK 认证方式。首先,对于多次尝试连接设备的终端,SAE 会直接拒绝服务,断绝了穷举或逐一尝试密码的行为;其次,SAE 的前向保密功能使得攻击者即使通过某种方式获取了密码,也不能破解获取到数据:最后,SAE 将设备视为对等的,任意一方都可以发起握手,独立地发送认证消息,缺少了来回交换消息的过程,从而让 KRACK 无可乘之机。
    • 提供可选的192位强度模式,进一步提升了密码防御强度:使用 HMAC-SHA-384 算法在四次握手阶段进行密钥导出和确认: 使用 GCMP-256(Galois Counter Mode Protocol,伽罗瓦计数器模式协议)算法保护用户上线后的无线流量;使用更加安全的 GCMP 的 GMAC-256(Galois Message Authentication Code,伽罗瓦消息认证码)保护组播管理帧。
    • 提供开放性网络保护,在该认证方式下,用户仍然无须输入密码即可接入网络,保留了开放式 Wi-Fi 网络用户接入的便利性。同时,OWE 采用 Diie-Hellman 密钥交换算法在用户和 Wi-Fi 设备之间交换密,为用户与 Wi-Fi 网络的数据传输进行加密,可以保护用户数据的安全性。

3. 关联
当AP 向 STA 返回认证响应消息,身份认证获得通过后,进入关联阶段。以便获得网络的完全访问权。关联的步骤如下图所示。

STA AP 关联请求 关联响应 数据 STA AP

4. 身份认证
在经过 Wi-Fi 的扫描、认证、关联后,我们将重点关注 Wi-Fi 连接的最后一个步骤,即身份验证。首先介绍 EAP(Extensible Authentication Protocol),然后介绍密协商(四次握手协议)。

  1. EAP: 目前在身份验证方面最基础的安全协议就是 EAP,它既是一种协议,更是一种协议框架。基于这个协议框架,各种认证方法都可得到很好的支持。当验证申请者通过 EAPOL(EAP Over LAN,基于LAN 的扩展 EAP 协议)发送身份验证请求给验证者时,如果验证成功,Supplicant就可正常使用网络了。EAP的架构如下图所示。
    在这里插入图片描述

    本教程主要介绍其中涉及的基本概念,不对 EAP 做深入的介绍,可通过 RFC 3748了解其详细情况。

    • DAuthenticator(验证者): 响应认证请求的实体。在无线网络中,AP 即 Authenticator.
    • Supplicant(验证申请者): 发起验证请求的实体。在无线网络中,STA 即 Supplicant。
    • BAS(Backend Authentication Server,后台认证服务器): 某些情况下(如企业级应用)Authenticator 并不真正处理身份验证,它仅仅将验证请求发给后台认证服务器去处理。正是这种架构设计拓展了 EAP 的适用范围。
    • AAA(Authentication、Authorization and Accounting,认证、授权和计费): 另外一种基于EAP 的协议。实现它的实体属于 BAS 的一种具体形式,AAA 包括常用的 RADIUS 服务器等
    • EAP Server: 真正处理身份验证的实体。如果没有 BAS,则 EAP Server 功能就在Authenticator 中,否则该功能由 BAS 实现。
  2. 密钥协商: RSNA(Robust Secure Network Association,强健安全网络联合)是IEEE 802.11定义的一组保护无线网络安全的过程,包括两个主要部分:数据加密和完整性校验。RSNA使用了前面提到的 TKIP 和 CCMP。TKIP 和 CCMP 中使用的 TK (Temporary Key)来自于RSNA 定义的密钥派生方法。同时,RSNA 基于IEEE 802.1X提出了4-Way Handshake (四次握手协议,用于派生对单播数据加密的密钥)和 Group Key Handshake (组密钥握手协议,用于派生对组播数据加密的密钥) 两个新协议,用于密钥派生。

    为什么要进行密钥派生呢?在 WEP 中,所有的STA 都使用同一个 WEP Key 进行数据加密其安全性较差。而 RSNA 要求不同的 STA和AP 关联后使用不同的 Key 进行数据加密。这是否表明AP 需要为不同的 STA 设置不同的密码呢?显然,这和实际情况是违背的,因为在实际生活中,我们将多个 STA 关联到同一个 AP 时使用的是相同的密码。

    如何实现不同 STA 使用不同密码呢?原来,我们在 STA 中设置的密码称为 PMK(PairwiseMaster Key,成对主密钥),其来源于 PSK,即在家用无线路由器里边设置的密码,无须专门的验证服务器,对应的设置项为 WPA/WPA2-PSK。PMK 的来源如下图所示。
    在这里插入图片描述

在WPA2-PSK 中,PSK 即PMK,直接来源于密;WPA3 则根据WPA2中的PMK通过SAE生成新的 PMK,以保证任何 STA 在不同的阶段都有不同的 PMK。通过SAE 获得 PMK 的程如下图所示。
在这里插入图片描述

SAE不区分Supplicant 或者 Authenticator,通信双方是对等的,均可首先发起认证。通过双方交换的数据,证明自己知道密钥,并生成PMK。SAE包括Commit 和Confirm两个阶段,在Commit 阶段,双方发送SAE Commit 帧互相提供数据来推测 PSK:在Confirm阶段,双方发送SAE Confir 顿互相确认推测的结果。通信双方校验成功后,进行后续的关联过程。
  1. Commit阶段: 发送端首先根据PSK、收发双方的MAC地址,通过 Huntingand Pecking算法计算出PWE(Password Element,密码元素);然后根据PWE、内部生成的随机数,通过椭圆曲线算法获得一个大整数Scalar 和圆曲线上一点的坐标 Element。接收方在对SAE Confirm倾校验通过后,使用本端和对端的 Scalar 等内容,通过密钥衍生算法计算出KCK(Key ConfirmationKey,密钥确认密钥)和PMK,其中KCK会在Confirm 阶段生成并校验中的内容。

  2. Confirm 阶段: 通信双方使用在 Commit 阶段产生的 KCK、本端和对端的 Scalar、本端和对端的 Element 等参数,使用相同的哈希消息认证算法,分别计算一个校验码,在双方校验码一致时,视为验证通过。

STA和AP在得到 PMK 后,将进行密钥派生。正是在密钥派生的过程中,AP 和不同STA生成了独特的密钥,这些密钥被设置到硬件中,用于实际数据的加/解密。由于 AP和STA 在每次关联时都需要重新派生这些密钥,所以它们称为 PTK(Pairwise Transient Key,成对临时Key)。二者利用 EAPOL Key 进行双方的 Nonce 等消息交换,这就需要使用到4-WayHandshake,其流程如下图所示。
在这里插入图片描述

  1. DAuthenticator生成一个Nonce(ANonce),然后利用EAPOL-Key 消息将其发给 Supplicant。

  2. Supplicant 根据 ANonce、自己生成一个 Nonce (SNonce)、自己所设置的 PMK和Authenticator 的 MAC 地址等信息进行密派生。随后将 SNonce 以及一些消息通过第二个EAPOL-Key 发送给 Authenticator。Message 2 还包含一个MIC值,该值会被 KCK 加密。Authenticator 取出 Message 2中的 SNonce 后,将进行和 Supplicant 中类似的计算来验证Supplicant 返回的消息是否正确。如果不正确,则表明 Supplicant 的PMK 错误,整个握手工作就此停止。

  3. 如果 Supplicant的 PMK 正确,则 Authenticator 也进行密钥派生。此后,Authenticator 将发送第三个EAPOL-Key给 Supplicant,该消息携带组临时密码(Group Transient Key,GTK,用于后续更新组密钥,该密钥用KEK 加密)、MIC(用 KCK 加密)。Supplicant 收到 Message3后也将做一些计算,以判断AP的PMK 是否正确。

  4. Supplicant 最后发送一次 EAPOL-Key 给 Authenticator 用于确认。此后,双方将使用它来对数据进行加密。

至此,Supplicant和Authenticator 完成密钥派生和组对,双方可以正常进行通信了。

ESP32 的 Wi-Fi

ESP32-S3 是一款通过升级双核 Xtensa LX7 CPU 和集成可编程 RF 前端、Wi-Fi 协议栈、TCP/IP 协议栈、硬件加速加密引擎等功能而实现的高性能、低功耗的 Wi-Fi 片上系统。它的 Wi-Fi 接口支持 IEEE 802.11 b/g/n/e/i 标准和更高速的 Wi-Fi 6 (802.11ax)新一代标准。

ESP32-S3 的 RF 前端是可编程的,可以支持多种 Wi-Fi 频段和信道宽度。这种设计具有更好的灵活性和适应性,可以满足不同的应用场景需求,其 Wi-Fi 接口支持多种 Wi-Fi 模式,包括 STA(Station)、AP(Access Point)、STA+AP 模式。此外,它还具备创新的连接方式,如 Wi-Fi Protected Setup(WPS) 一键连网,方便用户快速连接 Wi-Fi 网络,并支持多种 Wi-Fi 加密算法和身份验证方式。它支持 WPA/WPA2-PSK、WPA/WPA2-Enterprise、802.1X/EAP 等协议,可以保证数据传输的安全性,同时还支持更高级别的安全机制,如 TLS/SSL。

ESP32-S3 的 Wi-Fi 接口还具有低功耗的设计。它可以通过设置 Wi-Fi 睡眠间隔、功率控制等方式实现低耗电,这一特性可以延长设备的使用寿命。

ESP3-S3 Wi-Fi 编程模型

ESP32-S3 Wi-Fi 编程模型如下图所示:
在这里插入图片描述

Wi-Fi 驱动程序可以被认为是一个对高层代码一无所知的黑盒子,例如 TCP/IP 堆栈、应用程序任务和事件任务。应用程序任务(代码)通常调用Wi-Fi 驱动程序 API来初始化 Wi-Fi 并在必要时处理 Wi-Fi 事件。Wi-Fi 驱动程序接收 API 调用、处理它们并将事件发布到应用程序。

Wi-Fi 事件处理基于esp_event 库。事件由 Wi-Fi 驱动程序发送到默认事件循环。应用程序可以在使用注册的回调中处理这些事件esp_event_handler_register()。Wi-Fi 事件也由esp_netif 组件处理以提供一组默认行为。例如,当 Wi-Fi 站点连接到 AP 时,esp_netif 会默认自动启动 DHCP 客户端。

Wi-Fi 通用启动流程

Main task APP task Event task LwIP task WiFi task 1. 初始化阶段 1.1 创建/初始化LwIP 1.2 创建/初始化事件环 1.3.1 创建/初始化 网络接口 1.3.2 创建/初始化 Wi-Fi 1.4 创建应用程序任务 2. 配置阶段 2.1 配置 Wi-Fi 3. 启动阶段 3.1 启动 Wi-Fi 3.2 WIFI_EVENT_STA_START 3.3 WIFI_EVENT_STA_START 4. 连接阶段 4.1 连接 Wi-Fi 4.2 WIFI_EVENT_STA_CONNECTED 4.3 WIFI_EVENT_STA_CONNECTED 5. 断开连接 5.1 WIFI_EVENT_STA_DISCONNECTED 5.2 WIFI_EVENT_STA_DISCONNECTED 5.3 断开连接 6. 清理阶段 6.1 断开 Wi-Fi连接 6.2 终止 Wi-Fi 6.1 清理 Wi-Fi Main task APP task Event task LwIP task WiFi task

初始化 Wi-Fi

无论是启动 Wi-Fi AP模式 或者是 Station 模式,也不论是扫描还是连接收发信息,第一步首先要启动 Wi-Fi,也就是上图中第一步骤。
第一部中总共分了4小步,以下逐步介绍。

0. 初始化 NVS

在 ESP32 系统中,使用 Wi-Fi 功能需要使用非易失存储(NVS)来保存一些 Wi-Fi 相关的配置信息,例如设备的 MAC 地址、运行时状态等信息。而在 ESP32 中,NVS 是通过 Flash 存储来实现的。
在使用 NVS 之前,需要先进行初始化,以确保 Flash 块已经被分区,并且预留了足够的空间供 NVS 使用。否则,在调用 Wi-Fi 函数时可能会出现错误,导致 Wi-Fi 功能无法正常使用。
因此,在使用 Wi-Fi 之前,通常需要先调用 nvs_flash_init() 函数来初始化 NVS 子系统。该函数会检查当前设备上的 Flash 分区情况,并根据需要执行格式化、扇区划分等操作,最终为 NVS 分配合适的存储空间。一般建议在系统启动时调用该函数完成初始化。
需要注意的是,不同的应用程序可能需要保存不同的 NVS 配置信息。一般情况下,这些信息需要在编写应用程序时提前定义好,并在程序初始化时自动加载到 NVS 中。在 ESP32 中,可以使用 nvs_set_blob() / nvs_get_blob() 等函数来进行 NVS 数据的读写操作,以实现应用程序数据和配置信息的持久化。

初始化 NVS 使用函数 nvs_flash_init() ,该函数在 NVS 章节已经有过介绍。

1. 创建和初始化 LwIP

LwIP 是轻量级的 TCP/IP 协议栈,是开源社区为嵌入式系统开发而开发的网络协议栈。LwIP 实现了 TCP/IP 网络协议的核心功能,包括 IP、TCP、UDP、ICMP、DNS 等协议。
LwIP 的特点是体积小、速度快、易于移植,它设计时考虑到了嵌入式系统的资源有限和系统复杂度低的特点,因此代码量很少,内存占用和处理器占用率极低。在嵌入式系统的网络应用中,LwIP 占据了重要的地位,不仅支持各种处理器架构和操作系统,而且还提供了许多良好的网络协议栈实现和深度优化,以满足不同的应用需求。
在 ESP-IDF 中,LwIP 是默认的网络协议栈,包含了 IPv4 和 IPv6 支持,支持 socket 接口等常见网络编程接口,并且提供了丰富的配置选项,以便用户根据自己的需要进行定制和优化。同时,ESP-IDF 还提供了丰富的网络应用示例和相关文档,方便用户快速了解和使用 LwIP 协议栈。

初始化 LwIP 协议栈使用的是 esp_netif_init() 函数,该函数不会传入参数,返回值 ESP_OK 成功,ESP_FAIL 表示失败。

2. 创建和初始化事件环

Event loop(事件环,也称为消息循环)是一种常见的事件驱动编程模型。它的主要思想是将应用程序的执行过程分解成多个事件任务并依次处理,从而实现对各类事件的有效响应。在 Event loop 中,事件通常由操作系统或其他应用程序组件发起,并被异步地发送到事件队列中,等待被服务。

Event loop 通常由一个无限循环构成,在每次循环中,应用程序会从事件队列中获取一个待处理的事件并处理它,然后继续等待下一个事件的到来。同时,Event loop 还具有灵活性和可定制性,可以根据不同的需求进行定制和优化。

在事件循环的架构中,需要明确以下几个概念:

  • 事件源:事件源是指能够产生事件的组件或实体,例如 IO 设备、定时器、网络套接字、用户界面控件等。
  • 事件类型:事件类型是指事件的种类或类型,例如读取文件、发送数据、UI 点击等。
  • 事件监听器:事件监听器是指负责相应事件的处理函数或方法,通常是由应用程序开发者编写的回调函数或者事件处理器。
  • 事件循环:事件循环是指事件处理过程的核心部分,负责将事件从事件队列中取出并将其分发给事件监听器进行处理。

在实际编程过程中,Event loop 通常与异步编程技术一起使用。通过将不同的事件注册到事件循环中,应用程序可以实现高效且响应迅速的处理能力,以提高系统的性能和用户体验。同时,应用程序也可以根据需要对事件循环进行调度和控制,以达到更好的效果。

在 Wi-Fi 中 事件环起着关键性的作用,因为 Wi-Fi 通讯中涉及多方,包括协议栈、底层驱动、用户线程等,如果这些线程间直接通讯就显得比较繁琐,所以使用事件环作为中介媒体进行消息传递,所有的消息都在这里汇聚,又都在这里进行分发,这样 Wi-Fi 编程就会变得非常简单。

事件环有很多种,本次初始化我们使用系统提供的一个默认事件环即可,创建函数是 esp_event_loop_create_default()

3. 创建和初始化网络接口

事件环创建完毕后,即可通过 esp_netif_create_default_wifi_ap()esp_netif_create_default_wifi_sta() 创建默认网络接口实例绑定站或具有 TCP/IP 堆栈的 AP,之后的通讯全靠这个接口。
该函数会返回一个 esp_netif_t 类型的句柄,就是网络接口的句柄。

4. 初始化 Wi-Fi 驱动

Wi-Fi 驱动初始化需要一个复杂的结构体 wifi_init_config_t,其原型如下:

typedef struct {
    system_event_handler_t event_handler;          /**< WiFi event handler */
    wifi_osi_funcs_t*      osi_funcs;              /**< WiFi OS functions */
    wpa_crypto_funcs_t     wpa_crypto_funcs;       /**< WiFi station crypto functions when connect */
    int                    static_rx_buf_num;      /**< WiFi static RX buffer number */
    int                    dynamic_rx_buf_num;     /**< WiFi dynamic RX buffer number */
    int                    tx_buf_type;            /**< WiFi TX buffer type */
    int                    static_tx_buf_num;      /**< WiFi static TX buffer number */
    int                    dynamic_tx_buf_num;     /**< WiFi dynamic TX buffer number */
    int                    cache_tx_buf_num;       /**< WiFi TX cache buffer number */
    int                    csi_enable;             /**< WiFi channel state information enable flag */
    int                    ampdu_rx_enable;        /**< WiFi AMPDU RX feature enable flag */
    int                    ampdu_tx_enable;        /**< WiFi AMPDU TX feature enable flag */
    int                    amsdu_tx_enable;        /**< WiFi AMSDU TX feature enable flag */
    int                    nvs_enable;             /**< WiFi NVS flash enable flag */
    int                    nano_enable;            /**< Nano option for printf/scan family enable flag */
    int                    rx_ba_win;              /**< WiFi Block Ack RX window size */
    int                    wifi_task_core_id;      /**< WiFi Task Core ID */
    int                    beacon_max_len;         /**< WiFi softAP maximum length of the beacon */
    int                    mgmt_sbuf_num;          /**< WiFi management short buffer number, the minimum value is 6, the maximum value is 32 */
    uint64_t               feature_caps;           /**< Enables additional WiFi features and capabilities */
    bool                   sta_disconnected_pm;    /**< WiFi Power Management for station at disconnected status */
    int                    espnow_max_encrypt_num; /**< Maximum encrypt number of peers supported by espnow */
    int                    magic;                  /**< WiFi init magic number, it should be the last field */
} wifi_init_config_t;
成员描述
event_handlerWiFi 事件处理函数指针,用于接收和处理 WiFi 相关的事件,例如连接网络、断开连接等。
osi_funcsWiFi 系统接口函数指针,包含一系列 WiFi 操作的系统接口函数,如发送数据、接收数据等。
wpa_crypto_funcs支持 WiFi 使用 WPA/WPA2 加密时需要传递的加密函数结构体。
static_rx_buf_num静态 RX 缓冲区数量,表示预先分配的 RX 缓冲区数目。
dynamic_rx_buf_num动态 RX 缓冲区数量,表示在运行时动态分配的 RX 缓冲区数目。
tx_buf_typeWiFi TX 缓冲区类型,可以是固定长度的或者动态分配的。
static_tx_buf_num静态 TX 缓冲区数量,表示预先分配的 TX 缓冲区数目。
dynamic_tx_buf_num动态 TX 缓冲区数量,表示在运行时动态分配的 TX 缓冲区数目。
cache_tx_buf_numWiFi TX 缓存区数量,表示缓存区的数量,TX 数据会先存储到缓存区,再由缓存区发送出去。
csi_enable用于 Wi-Fi 信道状态信息的启用标志,表示是否启用 Wi-Fi 信道状态信息功能。
ampdu_rx_enableAMPDU RX 功能启用标志。
ampdu_tx_enableAMPDU TX 功能启用标志。
amsdu_tx_enableAMSDU TX 功能启用标志。
nvs_enableNVS 存储支持标志,表示是否启用使用非易失性存储(NVS)存储 Wi-Fi 配置的选项。
nano_enableNano 选项启用标志,表示是否启用 Nano 选项。
rx_ba_winBlock Ack(块确认) RX 窗口大小。
wifi_task_core_idWi-Fi 任务运行所在的核心 ID。
beacon_max_lenAP 模式下 Beacon 数据帧的最大长度。
mgmt_sbuf_num管理短缓冲区的数量,最小值为 6,最大值为 32。
feature_caps使能额外的 WiFi 功能和能力。
sta_disconnected_pmWiFi 掉线时的电源管理设置。
espnow_max_encrypt_numESP-NOW 支持最大加密数量。
magicWiFi 初始化所需的魔术数,应该是最后一个成员。

这个结构体过于复杂, 当前阶段我们先不对他做分析,可以直接通过 WIFI_INIT_CONFIG_DEFAULT() 的宏定义获得一个默认的配置结构体。 然后调用 esp_wifi_init() 对 Wi-Fi 进行初始化

届时,第一阶段的任务完成,完整代码如下:

// 0. 初始化 NVS
ESP_ERROR_CHECK(nvs_flash_init());      // 使用Wifi之前必须先初始化 NVS,否则报错
ESP_LOGI(TAG, "NVS 初始化完毕");

// 1. 调用 esp_netif_init() 创建 LwIP 核心任务,初始化LwIP相关工作
ESP_ERROR_CHECK(esp_netif_init());
ESP_LOGI(TAG, "网络 初始化完毕");

// 2. 调用esp_event_loop_create()创建系统Event任务(事件环),初始化应用事件回调函数。
ESP_ERROR_CHECK(esp_event_loop_create_default());
ESP_LOGI(TAG, "事件环 创建完毕");

// 3. 调用esp_netif_create_default_wifi_ap()或esp_netif_create_default_wifi_sta()创建默认网络接口实例绑定站或具有 TCP/IP 堆栈的 AP。
netif_sta = esp_netif_create_default_wifi_sta();        // 创建默认的Wifi连接
ESP_LOGI(TAG, "STA 连接创建完毕");

// 4. 调用esp_wifi_init()创建Wi-Fi驱动任务和初始化Wi-Fi驱动。
wifi_init_config_t  cfg = WIFI_INIT_CONFIG_DEFAULT();   // 创建默认的配置函数
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
ESP_LOGI(TAG, "WiFi 初始化完毕");

配置 Wi-Fi

该阶目前只有一个任务,通过 esp_wifi_set_mode() 设置 Wi-Fi 为 AP 或者是 SAT 模式,该函数参数有三个选项:

  • WIFI_MODE_STA: 设置 Wi-Fi 为工作站模式,用于连接热点
  • WIFI_MODE_AP: 设置 Wi-Fi 为热点模式,等待工作站的接入
  • WIFI_MODE_APSTA: 热点加工作站模式,是上面两种的结合体

启动 Wi-Fi

esp_wifi_start()

Wi-Fi 扫描

在 Wi-Fi 环境中,如果想获得周围的 AP 热点,则需要用到 Wi-Fi 扫描技术,当 Wi-Fi 设备在扫描模式下时,会搜索附近所有的 Wi-Fi 信号,并收集相关的网络参数,如 SSID、信道、安全协议等信息,并将其显示出来。
Wi-Fi 扫描模式一共分为两种:

  • 主动扫描:主动扫描是指 Wi-Fi 设备主动发送探测帧来探测附近可见的 Wi-Fi 网络。主动扫描需要消耗一定的电量和资源,但能够获取更全面的 Wi-Fi 网络信息。
  • 被动扫描:被动扫描是指 Wi-Fi 设备在监听模式下,通过接收周围的 Wi-Fi 信号来获取网络信息。被动扫描不需要主动发送探测帧,因此不会消耗设备的电量和资源,但是可能无法获取到所有的 Wi-Fi 信息。

ESP32-S3 中,Wi-Fi 扫描分为以下几种:

模式描述
主动扫描通过发送探测请求进行扫描。默认扫描是主动扫描。
被动扫描没有发送探测请求。只需切换到特定频道并等待信标即可。应用程序可以通过 wifi_scan_config_t 的 scan_type 字段启用它。
前台扫描当 Station 模式下没有 Wi-Fi 连接时,此扫描适用。前台或后台扫描由 Wi-Fi 驱动程序控制,不能由应用程序配置。
后台扫描此扫描适用于在 Station 模式或 Station+AP 模式下有 Wi-Fi 连接时。是前台扫描还是后台扫描取决于 Wi-Fi 驱动程序,不能由应用程序配置。
全通道扫描它扫描所有频道。如果wifi_scan_config_t的channel字段设置为0,则为全通道扫描。
特定通道扫描它仅扫描特定频道。如果wifi_scan_config_t的channel字段设置为1,则为特定频道扫描。

上表中的扫描方式可以任意组合,所以我们一共有8种不同的扫描方式:

  • 全通道后台主动扫描
  • 全通道背景被动扫描
  • 全通道前台主动扫描
  • 全通道前台被动扫描
  • 特定通道后台主动扫描
  • 特定通道背景被动扫描
  • 特定频道前台主动扫描
  • 特定频道前台被动扫描
全通道扫描(Scan All APs on All Channels (Foreground))

前台扫描用于 Station 模式下,还没有连接Wi-Fi 热点的情况下进行的扫描,这时候扫描线程将会把所有时间片用在扫描上,而不用关心现有通道(因为没有连接)的数据收发,它具体的流程如下:

APP task Event task WiFi task 1. 配置阶段 1.1 配置国家代码 1.2 扫描参数配置 2. 扫描阶段 2.1 Scan channel 1 2.2 Scan channel 2 Scan channel ... 2.x Scan channel N 3. 完成阶段 3.1WIFI_EVENT_SCAN_DONE 3.2WIFI_EVENT_SCAN_DONE APP task Event task WiFi task
启动扫描

根据上图中 Wi-Fi 前端扫描顺序,第一步先要配置国家代码,可通过 esp_wifi_set_country() 函数进行设置,该函数需要传入一个 wifi_country_t 类型的结构体,该结构体原型如下:

typedef struct {
    uint8_t cc[2];  /**< country code string, follow ISO/IEC 3166-1 alpha-2, i.e. "CN", "US". */
    uint8_t schan;   /**< start channel */
    uint8_t nchan;   /**< total channel number */
    uint16_t max_tx_power;  /**< maximum TX power in milli-dBm */
} wifi_country_t;
成员描述
cc2 字节长度的国家代码字符串,遵循 ISO/IEC 3166-1 alpha-2 标准,例如 “CN” 表示中国,“US” 表示美国。
schan起始频道号,表示当前国家/地区允许的 Wi-Fi 工作频段的起始频道号。
nchan频道数量,表示当前国家/地区允许使用的 Wi-Fi 频段中的可用频道总数。
max_tx_power最大发射功率,以毫分贝毫瓦(mW)为单位。

国家代码需要根据 IEEE 标准 802.11-2020 的附录 E,中国的缩写是 CN。
通道从1开始扫描,一共扫描13个。
该部分代码配置如下:

wifi_country_t country_cfg = {
    .cc = "CN",                             // 国家代码
    .schan = 1,                             // 开始扫描的通道号
    .nchan = 13,                            // 要扫描的频道号数量
    .policy = WIFI_COUNTRY_POLICY_AUTO      // 最大发射功率
};
// 1. 配置扫描国家项
esp_wifi_set_country(&country_cfg);

扫描类型和其他每次扫描属性由配置 esp_wifi_scan_start() ,该函数原型如下:

esp_err_t esp_wifi_scan_start(const wifi_scan_config_t *config, bool block);

该函数传入两个参数,分别是 扫描配置项,以及 是否以阻塞方式开启扫描,block 选择 pdTRUE,则只会在扫描完成之后返回。
wifi_scan_config_t 为配置项的结构体,原型如下:

typedef struct {
    uint8_t *ssid;               /**< SSID of AP */
    uint8_t *bssid;              /**< MAC address of AP */
    uint8_t channel;             /**< channel, scan the specific channel */
    bool show_hidden;            /**< enable to scan AP whose SSID is hidden */
    wifi_scan_type_t scan_type;  /**< scan type, active or passive */
    wifi_scan_time_t scan_time;  /**< scan time per channel */
} wifi_scan_config_t;
成员描述
ssid用于设置要扫描的网络的 SSID。如果为 NULL,则会扫描所有可见的网络。
bssid用于设置要扫描的 AP 的 BSSID(MAC 地址)。如果为 NULL,则会扫描所有可见的 AP。
channel用于指定要扫描的频道。如果为 0,则表示扫描所有频道。
show_hidden是否扫描隐藏 SSID 的网络。若为 true,则可扫描到隐藏 SSID 的网络。
scan_type扫描的方式。可以为 ACTIVE 或 PASSIVE。默认为 ACTIVE。
scan_time扫描时间。如果 scan_type 为 ACTIVE,则此值表示主动扫描的最大时间。对于被动扫描,此参数则被忽略。

scan_type 是在 Wi-Fi 扫描时用来指定扫描方式的一个选项,可以有两个选项:

  • WIFI_SCAN_TYPE_ACTIVE:主动扫描。ESP32 在扫描时会发送探测请求来获取 AP 的信息。主动扫描可以获得更准确、实时的信号强度和其他信息,但是对 AP 产生流量负荷,同时也会消耗更多的能量。
  • WIFI_SCAN_TYPE_PASSIVE:被动扫描。ESP32 通过监听信道来收集 AP 的信号,而不会向 AP 发送探测请求。被动扫描消耗的能量较少,对 AP 产生的流量也比较小,但是无法获得实时的信号强度和其他信息,收集到的数据可能不够准确。

scan_time 是Wi-Fi 扫描的时间设置,是 wifi_scan_time_t 类型的结构体,其中包含两个成员:

  • active: 表示在主动扫描时,每个频道扫描的时间,单位为毫秒。在主动扫描中,无线 NIC 会主动向 AP 发送探测请求,获取更多的详细信息,例如 AP 的信号强度、安全级别等。由于主动扫描会产生较多的功耗和网络流量,因此需要适当地设定扫描时间,以达到良好的效果和最小化的能耗消耗。

  • passive: 表示在被动扫描时,每个频道扫描的时间,单位为毫秒。在被动扫描中,无线 NIC 不会向 AP 发送探测请求,而是 passively 监听无线信道,通过收集信道上的 Beacon 帧和 Probe Response 帧来获取 AP 的信息。由于不会产生数据流量,因此被动扫描的功耗和网络流量都比较小。但是,由于在被动扫描期间无线 NIC 必须保持 listening,因此扫描时间不能设置得太长,否则可能会对连接的稳定性产生负面影响,建议不超过 1500ms。

通常情况下,如果需要快速更新 Wi-Fi 状态或需要详细的诊断信息,应该使用主动扫描。如果只是为了节省电量,则可以使用被动扫描。

当前我们只需要关心 show_hidden 选项即可,把隐藏的网络也扫描出来,所以该部分代码如下:

 wifi_scan_config_t scan_cfg = {
        .show_hidden = true                     // 扫描隐藏项
    };
    // 2. 启动扫描
    esp_wifi_scan_start(&scan_cfg, true);

扫描完成之后,事件环中会产生一个 WIFI_EVENT_SCAN_DONE 的事件,如果是非阻塞扫描,我们可以到时间处理回调函数中获得这个事件并进行数据处理,这里我们用的是前台扫描,而且用的是非阻塞式的,所以等待扫描完成之后我们可以通通过另外一种方式获取数据。

扫描完毕后,可以通过 esp_wifi_scan_get_ap_num() 获取扫描到的热点数量。
通过 esp_wifi_scan_get_ap_records() 获得热点的详细信息,该函数传入两个参数,第一个参数为预计读取的最大数量,该参数是这个指针类型,调用完毕后回将本次获得的数量回写,第二个参数是 wifi_ap_record_t 类型的结构体数组,用于存放返回的热点信息,其原型如下:

typedef struct {
    uint8_t bssid[6];                     /**< MAC address of AP */
    uint8_t ssid[33];                     /**< SSID of AP */
    uint8_t primary;                      /**< channel of AP */
    wifi_second_chan_t second;            /**< secondary channel of AP */
    int8_t  rssi;                         /**< signal strength of AP */
    wifi_auth_mode_t authmode;            /**< authmode of AP */
    wifi_cipher_type_t pairwise_cipher;   /**< pairwise cipher of AP */
    wifi_cipher_type_t group_cipher;      /**< group cipher of AP */
    wifi_ant_t ant;                       /**< antenna used to receive beacon from AP */
    uint32_t phy_11b:1;                   /**< bit: 0 flag to identify if 11b mode is enabled or not */
    uint32_t phy_11g:1;                   /**< bit: 1 flag to identify if 11g mode is enabled or not */
    uint32_t phy_11n:1;                   /**< bit: 2 flag to identify if 11n mode is enabled or not */
    uint32_t phy_lr:1;                    /**< bit: 3 flag to identify if low rate is enabled or not */
    uint32_t wps:1;                       /**< bit: 4 flag to identify if WPS is supported or not */
    uint32_t ftm_responder:1;             /**< bit: 5 flag to identify if FTM is supported in responder mode */
    uint32_t ftm_initiator:1;             /**< bit: 6 flag to identify if FTM is supported in initiator mode */
    uint32_t reserved:25;                 /**< bit: 7..31 reserved */
    wifi_country_t country;               /**< country information of AP */
} wifi_ap_record_t;
成员描述
bssidAP 的 MAC 地址。类型为 uint8_t[6] 的数组,长度为 6。
ssidAP 的 SSID。类型为 uint8_t[33] 的数组,长度为 33。
primaryAP 的主信道号。类型为 uint8_t,取值范围为 1~14。
secondAP 的辅助信道号。类型为 wifi_second_chan_t,表示辅助信道,枚举值为 WIFI_SECOND_CHAN_NONE、WIFI_SECOND_CHAN_ABOVE 和 WIFI_SECOND_CHAN_BELOW。
rssiAP 的信号强度。类型为 int8_t,单位是 dBm,数值越大表示信号越强。
authmodeAP 的认证方式。类型为 wifi_auth_mode_t,表示认证方式,枚举值为 WIFI_AUTH_OPEN、WIFI_AUTH_WEP、WIFI_AUTH_WPA_PSK、WIFI_AUTH_WPA2_PSK 和 WIFI_AUTH_WPA_WPA2_PSK。
pairwise_cipherAP 使用的对称加密算法。类型为 wifi_cipher_type_t,表示对称加密算法,枚举值为 WIFI_CIPHER_TYPE_NONE、WIFI_CIPHER_TYPE_WEP40
group_cipherAP 使用的群组加密算法。类型为 wifi_cipher_type_t,表示群组加密算法,枚举值同上。
ant接收 AP 信号的天线类型。类型为 wifi_ant_t,表示天线类型,枚举值为 WIFI_ANT_ANT0、WIFI_ANT_ANT1 和 WIFI_ANT_MAX。
phy_11b标记 AP 是否支持 802.11b。类型为 uint32_t,只有最低位有效,值为 0 表示不支持,值为 1 表示支持。
phy_11g标记 AP 是否支持 802.11g。类型为 uint32_t,只有次低位有效,值为 0 表示不支持,值为 1 表示支持。
phy_11n标记 AP 是否支持 802.11n。类型为 uint32_t,只有第 3 个比特位有效,值为 0 表示不支持,值为 1 表示支持。
phy_lr标记 AP 是否启用低速率数据传输模式。类型为 uint32_t,只有第 4 个比特位有效,值为 0 表示未启用,值为 1 表示已启用。
wps标记 AP 是否支持 WPS。类型为 uint32_t,只有第 5 个比特位有效,值为 0 表示不支持,值为 1 表示支持。
ftm_responder标记 AP 是否支持 FTM 响应者模式。类型为 uint32_t,只有第 6 个比特位有效,值为 0 表示不支持,值为 1 表示支持。
ftm_initiator标记 AP 是否支持 FTM 发起者模式。类型为 uint32_t,只有第 7 个比特位有效,值为 0 表示不支持,值为 1 表示支持。
reserved保留字段,占用 25 位,值均为 0。
countryAP 所在国家的区域代码。类型为 wifi_country_t,表示国家信息,由两个字符组成,分别为大写字母 A~Z 或数字 0~9,如 “CN” 表示中国。

该部分代码如下:

// 获取扫描结果
uint16_t ap_num;        //扫描到 AP 的数量
ESP_ERROR_CHECK(esp_wifi_scan_get_ap_num(&ap_num));     // 获取 扫描到的 热点数量
ESP_LOGI(TAG, "扫描到热点数量: %d", ap_num);
// 输出 Wi-Fi 热点
uint16_t max_aps = 20;
wifi_ap_record_t ap_records[max_aps];
memset(ap_records, 0, sizeof(wifi_ap_record_t));    // 归零
uint16_t aps_count = max_aps;
ESP_ERROR_CHECK(esp_wifi_scan_get_ap_records(&aps_count, ap_records));  // 只取前 20 条热点的信息
printf("%20s   %s   %s   %s\n" ,"SSID", "频道", "强度", "MAC");
// 输出 热点信息
for(int i=0; i<aps_count; i++){
    printf("%30s    %3d    %3d    %02X-%02X-%02X-%02X-%02X-%02X\n", ap_records[i].ssid, ap_records[i].primary, ap_records[i].rssi, ap_records[i].bssid[0], ap_records[i].bssid[1], ap_records[i].bssid[2], ap_records[i].bssid[3], ap_records[i].bssid[4], ap_records[i].bssid[5]);
}

执行结果如下:
在这里插入图片描述

连接到 AP

处于 STA 模式下的 ESP32-S3,可以作为 STA 连接到 AP。
基于 AP 组建的 BSS,多个 STA 加入所组成的无线网络,AP 是这个那个网络的中心,网络中所有的通讯都经过 AP 转发来完成。再次模式下,设备可以通过 AP 分配 IP 地址(Internet Protocol Address,国际协议地址)直接访问外网和内网。
在连接阶段,我需要稍微对之前那个通用连接流程进行进一步的扩展:

Main task APP task Event task LwIP task WiFi task 1. 初始化阶段 1.1 创建/初始化LwIP 1.2 创建/初始化事件环 1.3.1 创建/初始化 网络接口 1.3.2 创建/初始化 Wi-Fi 1.4 创建应用程序任务 2. 配置阶段 2.1 配置 Wi-Fi 3. 启动阶段 3.1 启动 Wi-Fi 3.2 WIFI_EVENT_STA_ START 3.3 WIFI_EVENT_STA_START 4. 连接阶段 4.1 连接 Wi-Fi 4.2 WIFI_EVENT_STA_CONNECTED 4.3 WIFI_EVENT_STA_CONNECTED 5. 获取 IP 阶段 5.1 启动 DHCP 客户端 5.2 IP_EVENT_STA_GOT_IP 5.3 IP_EVENT_STA_GOT_IP 5.4 套接字初始化 6. 断开阶段 6.1 WIFI_EVENT_STA_DISCONNECTED 6.2 WIFI_EVENT_STA_DISCONNECTED 6.3 断开处理 7. IP 更改阶段 7.1 IP_EVENT_STA_GOT_IP 7.2 IP_EVENT_STA_GOT_IP 7.3 套接字错误处理 8. 清理阶段 8.1 断开 Wi-Fi连接 8.2 终止 Wi-Fi 8.3 清理 Wi-Fi Main task APP task Event task LwIP task WiFi task
1. 初始化阶段

在上面两个例子中,我们已经对初始化阶段进行了详细的讲解,在第二步我们只创建了一个事件环,而没有对其中的事件进行处理。
从时序图中可以看出,在设备连接和交互的过程中,在 LwIP 和 Wi-Fi 层面都会发送不同的事件到 Event task(事件环)中,事件环 进行事件分发,那事件环要把这些数据发送到哪里呢?
从图中看,大部分事件都发送到了 APP task 中,APP task 其实值得是一类任务,是用户用来处理回调事件的任务,不只是一个,但在例程中为了方便讲解,我们只启动了一个任务作为事件处理器。

Wi-Fi事件处理基于 esp_event 组件,Wi-Fi 驱动程序会将事件发送到默认事件循环。应用程序可以在 esp_event_handler_register() 中的回调函数中处理这些事件。esp_netif 组件还会处理 W-Fi 事件,以提供一组默认行为。
例如,当 ESP32-S3 连接到 AP 时,esp_netif 将自动启动 DHCP 客户端(默认情况下),我们会通过 esp_event_loop_create_default() 创建一个默认的事件环,并通过 esp_netif_create_default_wifi_sta() 创建默认网络接口实例绑定站或具有 TCP/IP 堆栈的 AP。

当 STA 链接 AP 成功后,会返回 WIFI_EVENT_STA_CONNECTED 事件,为了能够接收并处理这个事件,我们必须通过 esp_event_handler_instance_register() 对事件进行注册,该函数原型如下:

esp_err_t esp_event_handler_instance_register(esp_event_base_t event_base,
                                             int32_t event_id,
                                             esp_event_handler_t event_handler,
                                             void *event_handler_arg,
                                             esp_event_handler_instance_t *instance);
参数描述
event_base事件基础,类型为 esp_event_base_t。它指定了所需处理的事件组件(或者说模块),用于唯一标识一个事件来源。例如 “wifi” 或 “nvs” 等。可以通过调用 esp_event_loop_create_default() 函数来创建默认的事件循环处理器;
event_id事件 ID,类型为 int32_t。它指定了需要处理的具体事件,可以是事件组件中定义的枚举值,也可以是自定义事件;
event_handler事件处理程序,类型为 esp_event_handler_t。该参数是一个函数指针,指向用户自己定义的事件处理函数,用于处理事件;
event_handler_arg事件处理程序参数,类型为 void * 。当事件被触发时,此参数会被传递给事件处理程序;
instance返回的事件实例指针,类型为 esp_event_handler_instance_t * 。该参数会在函数执行成功后,被赋值为一个新的事件实例指针。事件实例是一个在事件循环过程中被创建的结构体,用于存储事件处理程序和其参数等信息。用户可以使用该实例来取消注册事件处理程序。

在本例中,我们需要关注以下事件:

  • WIFI_EVENT_STA_START: 如果esp_wifi_start()返回ESP_OK,且当前Wi-Fi模式为Station或AP+Station,则触发该事件。收到此事件后,事件任务将初始化 LwIP 网络接口 (netif)。一般需要调用应用事件回调esp_wifi_connect()来连接配置好的AP。

  • WIFI_EVENT_STA_CONNECTED: 如果esp_wifi_connect()返回 ESP_OK 并且站点成功连接到目标 AP,连接事件将发生。收到此事件后,事件任务将启动 DHCP 客户端并开始获取 IP 地址的 DHCP 过程。然后,Wi-Fi 驱动程序就可以发送和接收数据了。这一刻有利于开始应用程序工作,前提是应用程序不依赖于 LwIP,即 IP 地址。但是,如果应用程序是基于 LwIP 的,那么您需要等到got ip事件进来。

  • WIFI_EVENT_STA_DISCONNECTED: 连接断开事件,在以下情况下可能会生成此事件:

    • 当esp_wifi_disconnect(), 或esp_wifi_stop(), 或esp_wifi_deinit()被调用并且站点已经连接到 AP 时。
    • 调用when esp_wifi_connect(),但Wi-Fi驱动由于某些原因未能与AP建立连接,例如扫描未找到目标AP、认证超时等。如果有多个AP具有相同的SSID,站点无法连接所有找到的 AP 后引发断开连接事件。
    • 当Wi-Fi连接因特定原因中断时,如站点连续丢失N个信标、AP踢出站点、更改AP的认证方式等。

    收到此事件后,事件任务的默认行为是:

    • 关闭站点的 LwIP netif。
    • 通知 LwIP 任务清除导致所有套接字状态错误的 UDP/TCP 连接。对于基于套接字的应用程序,应用程序回调可以选择关闭所有套接字并在必要时在收到此事件后重新创建它们。

    应用程序中此事件最常见的事件处理代码是调用esp_wifi_connect()重新连接 Wi-Fi。但是,如果事件是因为esp_wifi_disconnect()被调用而引发的,则应用程序不应调用esp_wifi_connect()以重新连接。应用程序有责任区分事件是由esp_wifi_disconnect()其他原因引起的。有时需要更好的重连策略,参考Wi-Fi 重连和Wi-Fi 连接时扫描。

    另一件值得我们注意的事情是 LwIP 的默认行为是在收到断开连接时中止所有 TCP 套接字连接。大多数时候这不是问题。然而,对于一些特殊的应用,这可能不是他们想要的,考虑以下场景:

    应用程序创建一个 TCP 连接以维护每 60 秒发送一次的应用程序级保活数据。

    由于某些原因,Wi-Fi连接被切断,引发WIFI_EVENT_STA_DISCONNECTED 。根据当前的实现,所有的 TCP 连接都将被移除,并且 keep-alive socket 将处于错误状态。然而,由于应用程序设计者认为网络层不应该关心 Wi-Fi 层的这个错误,因此应用程序不会关闭套接字。

    五秒钟后,Wi-Fi 连接恢复,因为esp_wifi_connect()在应用程序事件回调函数中被调用。此外,站点连接到同一个 AP 并获??得与以前相同的 IPV4 地址。

    六十秒后,当应用程序使用 keep-alive 套接字发送数据时,套接字返回错误,应用程序关闭套接字并在必要时重新创建它。

    在上述场景中,理想情况下,应用程序套接字和网络层不应受到影响,因为 Wi-Fi 连接只是暂时失败并且恢复得非常快。应用程序可以通过 LwIP 菜单配置启用“IP 更改时保持 TCP 连接”。

  • IP_EVENT_STA_GOT_IP: 当 DHCP 客户端从 DHCP 服务器成功获取 IPV4 地址,或者 IPV4 地址发生更改时,会发生此事件。该事件意味着一切就绪,应用程序可以开始其任务(例如,创建套接字)。
    由于以下原因,IPV4 可能会更改:

    • DHCP客户端更新/重新绑定IPV4地址失败,站点IPV4重置为0。
    • DHCP 客户端重新绑定到不同的地址。
    • 静态配置的 IPV4 地址已更改。

    IPV4 地址是否更改由 的字段 ip_change 指示 ip_event_got_ip_t
    套接字是基于IPV4地址的,也就是说,如果 IPV4 发生变化,所有与这个IPV4相关的套接字都会出现异常。收到此事件后,应用程序需要关闭所有套接字并在 IPV4 更改为有效时重新创建应用程序。

注册事件代码如下:

esp_event_handler_instance_t instance_any_id;
esp_event_handler_instance_t instance_got_ip;
ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,
                                                    ESP_EVENT_ANY_ID,
                                                    &event_handler,
                                                    NULL,
                                                    &instance_any_id));
ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT,
                                                    IP_EVENT_STA_GOT_IP,
                                                    &event_handler,
                                                    NULL,
                                                    &instance_got_ip));

以上代码注册了两组事件,第一组关注所有来自 Wi-Fi 的时间,第二组则关注的是来自 LwIP 的获取 IP 事件。
与之对应的,我们需要创造一个 event_handler 回调处理函数,以及相关通知事件组,以便于当事件产生之后通过直接任务通知的方式告知等候线程(APP task)。
回调函数原型如下:

static void event_handler(void* arg, esp_event_base_t event_base,
                                int32_t event_id, void* event_data);
参数描述
arg回调时传入的参数,对应 esp_event_handler_instance_register 中的 event_handler_arg
event_base事件基础,类型为 esp_event_base_t。它指定了所需处理的事件组件(或者说模块),用于唯一标识一个事件来源。例如 “wifi” 或 “nvs” 等。可以通过调用 esp_event_loop_create_default() 函数来创建默认的事件循环处理器;
event_id事件 ID,类型为 int32_t。它指定了需要处理的具体事件,可以是事件组件中定义的枚举值,也可以是自定义事件;
event_data该事件可能携带的数据

该部分代码如下:

static void event_handler(void *arg, esp_event_base_t event_base,
                          int32_t event_id, void *event_data)
{
    if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START)
    {
        ESP_LOGI(TAG, "WiFi 启动成功,准备连接到 AP");
        esp_wifi_connect();
    }
    else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED)
    {
        if (s_retry_num < ESP_MAXIMUM_RETRY)
        {
            esp_wifi_connect();
            s_retry_num++;
            ESP_LOGI(TAG, "重新连接到 AP");
        }
        else
        {
            xTaskNotify(app_task, WIFI_FAIL_BIT, eSetBits);
        }
        ESP_LOGI(TAG, "连接 AP 失败");
    }
    else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP)
    {
        ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data;
        ESP_LOGI(TAG, "获取IP:" IPSTR, IP2STR(&event->ip_info.ip));
        s_retry_num = 0;
        xTaskNotify(app_task, WIFI_CONNECTED_BIT, eSetBits);
    }
}
  1. WIFI_EVENT_STA_START 事件到达的时候,马上启动 WiFi连接任务,在例程中,我们直接在回调函数中调用了 esp_wifi_connect() 方法连接 AP,在实际开发中,建业将这部分代码移至专属的 APP task 中。
  2. WIFI_EVENT_STA_DISCONNECTED 事件到达的时候,表示 热点连接失败,或者断开连接,在这里进行一个5次的重连,如果连接出现问题,则告知线程连接失败。
  3. IP_EVENT_STA_GOT_IP 事件到达的时候(注意,该事件属于 IP_EVENT 对于 event_base 项一定要加以判断,因为至判断 event_id 有可能出现不同事件基中的事件ID重复的可能),说明已经正确获取了IP,这时我们把IP打印出来,并通知APP线程IP获取成功。

该方法创建完毕后,我们将注册部分安装到第一步骤中创建事件环(esp_event_loop_create_defaul() 之后)。
然后再次回到时序图第一阶段,1.4 步中,需要创还能 APP task,这里可以是一个任务,也可以针对不同的时间处理分成不同的任务。
示例代码中,我们使用一个 app_task 代替。

/**
 * @brief 事件处理任务入口
*/
void task_evnet_app_entry(void *param){
    while(1){
        uint32_t notify_value;
        xTaskNotifyWait(0x00, 0x00, &notify_value, portMAX_DELAY);
        if (notify_value & WIFI_CONNECTED_BIT) {
            // 这里的成功指的是连接 AP 成功,并且成功获取了 IP地址
            ESP_LOGI(TAG, "Wi-Fi 成功连接到热点 SSID:%s", WIFI_SSID);   // Wi-Fi 连接成功
        } else if(notify_value & WIFI_FAIL_BIT) {
            ESP_LOGE(TAG, "W-Fi 连接热点失败 SSID:%s", WIFI_SSID);      // Wi-Fi 连接失败
        }
    }
}
/**
 * @brief 创建事件处理任务。
*/
void task_event_app_create()
{
    xTaskCreate(task_evnet_app_entry, "APP-Task", 1024*4, NULL, 10, &app_task);
}
2. 配置阶段

Wi-Fi 驱动程序初始化成功后,进入配置阶段。在该阶段中,Wi-Fi 启动程序处于 STA 模式,调用 esp_wifi_set_mode(WIFI_MODE_STA) 函数将 ESP32-S3 模式设置为 STA模式(这部分代码在上面扫描例程中已经做过了),然后如果这时候需要连接 AP 的话,需要通过 esp_wifi_set_config() 函数设置 ESP32-S3 的工作参数,该函数原型如下:

esp_err_t esp_wifi_set_config(wifi_interface_t interface, wifi_config_t *conf);
参数描述
interfaceWiFi 接口类型,类型为 wifi_interface_t。取值可以是 WIFI_IF_STA 表示设置 STA 模式配置参数,或者是 WIFI_IF_AP 表示设置 AP 模式配置参数;
confWiFi 配置参数,类型为 wifi_config_t * 。对于 STA 模式,表示 STA 的连接配置;对于 AP 模式,则表示 AP 的配置。
返回值如果返回值为 ESP_OK 则表示设置配置参数成功,否则表示设置失败。可能的错误码包括 ESP_ERR_INVALID_ARG(参数无效)、ESP_ERR_WIFI_NOT_INIT(Wi-Fi 初始化失败)、ESP_ERR_WIFI_IF(无效的接口类型)等。

使用该函数需要注意以下几点:

  • 此 API 只能在启用指定接口时调用,否则会失败;
  • 对于 STA 配置,bssid_set 必须为 0,除非用户需要检查 AP 的 MAC 地址;
  • ESP32 仅限于一个频道,因此在软AP + Station 模式中,软AP 将自动将其通道调整为与 ESP32 Station 相同的频道;
  • 配置将存储在 NVS 中。

该函数第二个参数需要传入一个 wifi_config_t 类型的配置项,该配置项可以配置 ESP32-S3 为 AP 类型、STA类型或者 AP+STA 类型,需要根据第一个参数判断并填写内容,这里我们对该配置进行简单设置,传入 SSID 和 Password,其他不做过多解释。

// 该函数用于替代扫描阶段的 wifi_configure
void wifi_sta_configure()
{
    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
    wifi_config_t wifi_cfg = {
        .sta = {
            .ssid = WIFI_SSID,
            .password = WIFI_PASSWORD
        }
    };
    ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_cfg));
    ESP_LOGI(TAG, "设置 Wi-Fi 为 Station 模式");
}

资源回收

当 Wi-Fi 使用完毕之后,一定要通过一系列操作回收资源,否则补单会影响其他任务对 Wi-Fi 的使用,还可能会造成内存泄漏。
回收资源的方式和注册时候正好相反。

1. 注销事件环

esp_event 事件环不仅对 Wi-Fi 事件进行监控,还会对蓝牙等其他设备事件进行操作,所以当 Wi-Fi 资源被回收时,首要的事就是注销事件环监控。
注销事件环监控使用 esp_event_handler_instance_unregister() 函数,该函数原型如下:

esp_err_t esp_event_handler_instance_unregister(esp_event_base_t event_base,
                                                int32_t event_id,
                                                esp_event_handler_instance_t instance);
参数描述
event_base事件基础,类型为 esp_event_base_t。它指定了所需处理的事件组件(或者说模块),用于唯一标识一个事件来源。例如 “wifi” 或 “nvs” 等。可以通过调用 esp_event_loop_create_default() 函数来创建默认的事件循环处理器;
event_id事件 ID,类型为 int32_t。它指定了需要处理的具体事件,可以是事件组件中定义的枚举值,也可以是自定义事件;
event_handler事件处理程序,类型为 esp_event_handler_t。该参数是一个函数指针,指向用户自己定义的事件处理函数,用于处理事件的删除;
2. 停止Wi-Fi

esp_wifi_stop()

3. 释放Wi-Fi

esp_wifi_deinit()

4. 清理 Wi-Fi 接口

清除所提供网络接口的默认wifi事件处理程序
esp_wifi_clear_default_wifi_driver_and_handlers()
这一步是对 esp_netif_create_default_wifi_sta() 的你想操作,所以要把创建函数返回的指针给销毁函数。

5. 释放 Wi-Fi 接口资源

相当于 free,并对 esp_netif_t 内部进行了深层次的 free。
esp_netif_destroy()

void wifi_destroy(){
    // 1. 注销事件环
    ESP_ERROR_CHECK(esp_event_handler_instance_unregister(IP_EVENT,
                                                        IP_EVENT_STA_GOT_IP,
                                                        instance_got_ip));
    ESP_ERROR_CHECK(esp_event_handler_instance_unregister(WIFI_EVENT,
                                                        ESP_EVENT_ANY_ID,
                                                        instance_any_id));
    // 2. 停止 Wi-Fi
    ESP_ERROR_CHECK(esp_wifi_stop());
    // 3. 释放 Wi-Fi
    ESP_ERROR_CHECK(esp_wifi_deinit());
    // 4. 清理 Wi-Fi 接口
    ESP_ERROR_CHECK(esp_wifi_clear_default_wifi_driver_and_handlers(netif_sta));
    // 5. 释放 Wi-Fi 接口资源
    esp_netif_destroy(netif_sta);
    ESP_LOGI(TAG, "Wi-Fi 资源释放完毕");
    // 最后销毁处理任务,这一步执行完毕后退出,所以不会执行后序内容
    vTaskDelete(app_task);
}

代码共享位置:http://192.168.172.17:3000/Mars.CN/ESP-IDF-S2-WiFi.git

作为 AP 启动

上面所有代码中,我们延时了作为 STA 模式启动 Wi-Fi 的完成流程,但在实际项目中,有可能 ESP32-S3 还会被作为热点启动。
还记得通用启动流程吗?
作为 AP 启动的流程基本上也是从通用启动流程演变过来的:

Main task APP task Event task LwIP task WiFi task 1. 初始化阶段 1.1 创建/初始化LwIP 1.2 创建/初始化事件环 1.3.1 创建/初始化 网络接口 1.3.2 创建/初始化 Wi-Fi 1.4 创建应用程序任务 2. 配置阶段 2.1 配置 Wi-Fi 3. 启动阶段 3.1 启动 Wi-Fi 3.2 WIFI_EVENT_AP_START 3.3 WIFI_EVENT_AP_START 4. 连接阶段 4.1 连接 Wi-Fi 4.2 WIFI_EVENT_AP_STACONNECTED 4.3 WIFI_EVENT_AP_STACONNECTED 4.4 设备权限处理 5. DHCP阶段 5.1 IP_EVENT_AP_STAIPASSIGNED 5.2 IP_EVENT_AP_STAIPASSIGNED 5.3 设备初始化 6. 断开连接 6.1 WIFI_EVENT_AP_STADISCONNECTED 6.2 WIFI_EVENT_AP_STADISCONNECTED 6.3 断开连接 7. 清理阶段 7.1 断开 Wi-Fi连接 7.2 终止 Wi-Fi 7.3 清理 Wi-Fi Main task APP task Event task LwIP task WiFi task

与 STA 代码的启动流程非常相似,只是我们在 3.1 阶段将用于 STA 接口创建的函数 esp_netif_create_default_wifi_sta() 换成了 AP 创建函数 esp_netif_create_default_wifi_ap() ,并且事件环初始的事件也有所不同,具体事件环代码如下:

static void event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data)
{
    if(event_base == WIFI_EVENT){
        if(event_id == WIFI_EVENT_AP_STACONNECTED){
            // 有设备接入
            // 获取设备 MAC 地址
            wifi_event_ap_staconnected_t* event = (wifi_event_ap_staconnected_t*) event_data;
            ESP_LOGI(TAG, "Station " MACSTR " 接入, AID=%d", MAC2STR(event->mac), event->aid);
        }
        else if(event_id == WIFI_EVENT_AP_STADISCONNECTED){
            // 有设备断开连接
            wifi_event_ap_stadisconnected_t* event = (wifi_event_ap_stadisconnected_t*) event_data;
            ESP_LOGI(TAG, "Station " MACSTR " 断开, AID=%d", MAC2STR(event->mac), event->aid);
        }
    }else if(event_base == IP_EVENT){
        if(event_id == IP_EVENT_AP_STAIPASSIGNED){
            ip_event_ap_staipassigned_t *event = (ip_event_ap_staipassigned_t *)event_data;
            ESP_LOGI(TAG, "分配IP :" IPSTR, IP2STR(&event->ip));
        }
    }
}

结合代码,根据时序图分析:

  • 当设备成功接入的时候会触发 WIFI_EVENTWIFI_EVENT_AP_STACONNECTED 事件,这时候可以从中获得设备的 MAC 地址信息(AID 是接入设备后的一个编号,跟设备无关),此时AP的 DHCP 启动自动分配IP,或者在此阶段也可以通过 tcpip_adapter_set_ip_info() 给设备分配一个固定IP。
  • 当设备断开与热点的连接时,会触发 WIFI_EVENTWIFI_EVENT_AP_STADISCONNECTED 事件,这里同样会携带一个 wifi_event_ap_stadisconnected_t 类型的属于,以表明是按个设备离开了。
  • 当设备被分派有效IP后,会触发 IP_EVENTIP_EVENT_AP_STAIPASSIGNED 事件,并传入 ip_event_ap_staipassigned_t 类型的参数,从中可以提取到为其分配的有效IP地址。

代码共享位置:http://192.168.172.17:3000/Mars.CN/ESP-IDF-S2-WiFi-AP.git

文章来源:https://blog.csdn.net/suolong123/article/details/135764092
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。