云计算百科
云计算领域专业知识百科平台

ESP32 使用ESP-IDF 连接WiFi并建立TCP服务器(支持多连接)通信源码分享

ESP32 使用ESP-IDF 连接WiFi并建立TCP服务器(支持多连接)通信源码分享

  • 一、源码分享
    • 1、效果展示
    • 2、开发环境搭建
    • 3、源码分享
  • 二、select关键字详解
    • 1、 核心原理
    • 2、函数原型
    • 3、关键操作宏
    • 4、 使用步骤
    • 5、示例代码
    • 6、优点与局限性
    • 7、总结
  • 三、ESP-IDF详解
    • 1、ESP32 概述
    • 2、 ESP-IDF 详解
      • 2.1 、ESP-IDF 的核心组件与架构
      • 2.2 、ESP-IDF 开发环境与工具链
      • 2.3 、ESP-IDF 开发流程简述
      • 2.4、 示例代码结构 (最简单的 Hello World)
      • 2.5、 优势与特点
      • 2.6 、适用场景
    • 3、总结

在这里插入图片描述

一、源码分享

1、效果展示

在这里插入图片描述

2、开发环境搭建

参考我这篇博文:VS Code 在线安装ESP-IDF,ESP32开发环境搭建详细教程

3、源码分享

#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "esp_system.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "nvs_flash.h"

#include "lwip/err.h"
#include "lwip/sys.h"
#include "esp_netif.h"
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include "lwip/err.h"
#include "lwip/sys.h"
#include "lwip/err.h"
#include "lwip/sockets.h"
#include "lwip/sys.h"
#include "lwip/netdb.h"

#define EXAMPLE_ESP_WIFI_SSID "VIP"
#define EXAMPLE_ESP_WIFI_PASS "nimingzi"
#define EXAMPLE_ESP_MAXIMUM_RETRY 5
#define PORT 8080
#define MAX_CLIENTS 10
#define BUFFER_SIZE 1024

#if CONFIG_ESP_STATION_EXAMPLE_WPA3_SAE_PWE_HUNT_AND_PECK
#define ESP_WIFI_SAE_MODE WPA3_SAE_PWE_HUNT_AND_PECK
#define EXAMPLE_H2E_IDENTIFIER ""
#elif CONFIG_ESP_STATION_EXAMPLE_WPA3_SAE_PWE_HASH_TO_ELEMENT
#define ESP_WIFI_SAE_MODE WPA3_SAE_PWE_HASH_TO_ELEMENT
#define EXAMPLE_H2E_IDENTIFIER CONFIG_ESP_WIFI_PW_ID
#elif CONFIG_ESP_STATION_EXAMPLE_WPA3_SAE_PWE_BOTH
#define ESP_WIFI_SAE_MODE WPA3_SAE_PWE_BOTH
#define EXAMPLE_H2E_IDENTIFIER CONFIG_ESP_WIFI_PW_ID
#endif
#if CONFIG_ESP_WIFI_AUTH_OPEN
#define ESP_WIFI_SCAN_AUTH_MODE_THRESHOLD WIFI_AUTH_OPEN
#elif CONFIG_ESP_WIFI_AUTH_WEP
#define ESP_WIFI_SCAN_AUTH_MODE_THRESHOLD WIFI_AUTH_WEP
#elif CONFIG_ESP_WIFI_AUTH_WPA_PSK
#define ESP_WIFI_SCAN_AUTH_MODE_THRESHOLD WIFI_AUTH_WPA_PSK
#elif CONFIG_ESP_WIFI_AUTH_WPA2_PSK
#define ESP_WIFI_SCAN_AUTH_MODE_THRESHOLD WIFI_AUTH_WPA2_PSK
#elif CONFIG_ESP_WIFI_AUTH_WPA_WPA2_PSK
#define ESP_WIFI_SCAN_AUTH_MODE_THRESHOLD WIFI_AUTH_WPA_WPA2_PSK
#elif CONFIG_ESP_WIFI_AUTH_WPA3_PSK
#define ESP_WIFI_SCAN_AUTH_MODE_THRESHOLD WIFI_AUTH_WPA3_PSK
#elif CONFIG_ESP_WIFI_AUTH_WPA2_WPA3_PSK
#define ESP_WIFI_SCAN_AUTH_MODE_THRESHOLD WIFI_AUTH_WPA2_WPA3_PSK
#elif CONFIG_ESP_WIFI_AUTH_WAPI_PSK
#define ESP_WIFI_SCAN_AUTH_MODE_THRESHOLD WIFI_AUTH_WAPI_PSK
#endif

static EventGroupHandle_t s_wifi_event_group;

#define WIFI_CONNECTED_BIT BIT0
#define WIFI_FAIL_BIT BIT1

static const char *TAG = "wifi station";

static int s_retry_num = 0;

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_wifi_connect();
} else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
if (s_retry_num < EXAMPLE_ESP_MAXIMUM_RETRY) {
esp_wifi_connect();
s_retry_num++;
ESP_LOGI(TAG, "retry to connect to the AP");
} else {
xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
}
ESP_LOGI(TAG,"connect to the AP fail");
} 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, "got ip:" IPSTR, IP2STR(&event->ip_info.ip));
s_retry_num = 0;
xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
}
}

void wifi_init_sta(void)
{
s_wifi_event_group = xEventGroupCreate();

ESP_ERROR_CHECK(esp_netif_init());

ESP_ERROR_CHECK(esp_event_loop_create_default());
esp_netif_create_default_wifi_sta();

wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));

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));

wifi_config_t wifi_config = {
.sta = {
.ssid = EXAMPLE_ESP_WIFI_SSID,
.password = EXAMPLE_ESP_WIFI_PASS,
.threshold.authmode = ESP_WIFI_SCAN_AUTH_MODE_THRESHOLD,
.sae_pwe_h2e = ESP_WIFI_SAE_MODE,
.sae_h2e_identifier = EXAMPLE_H2E_IDENTIFIER,
},
};
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA) );
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config) );
ESP_ERROR_CHECK(esp_wifi_start() );

ESP_LOGI(TAG, "wifi_init_sta finished.");

EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group,
WIFI_CONNECTED_BIT | WIFI_FAIL_BIT,
pdFALSE,
pdFALSE,
portMAX_DELAY);

if (bits & WIFI_CONNECTED_BIT) {
ESP_LOGI(TAG, "connected to ap SSID:%s password:%s",
EXAMPLE_ESP_WIFI_SSID, EXAMPLE_ESP_WIFI_PASS);
} else if (bits & WIFI_FAIL_BIT) {
ESP_LOGI(TAG, "Failed to connect to SSID:%s, password:%s",
EXAMPLE_ESP_WIFI_SSID, EXAMPLE_ESP_WIFI_PASS);
} else {
ESP_LOGE(TAG, "UNEXPECTED EVENT");
}
}

void tcp_server_loop()
{
char buffer[BUFFER_SIZE];
int client_socket[MAX_CLIENTS] = {0};
struct sockaddr_in serverAddr, clientAddr;
fd_set allFdSet;
int listenFd;
fd_set readSet;
int maxFd;
int nReady;
socklen_t sinSize;
// 创建TCP套接字
if ((listenFd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
ESP_LOGE(TAG, "Socket creation failed");
return;
}

// 设置套接字选项
/* if (setsockopt(listenFd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt)))
{
ESP_LOGE(TAG, "Setsockopt failed");
close(listenFd);
return;
}*/

serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = INADDR_ANY;
serverAddr.sin_port = htons(PORT);

// 绑定地址和端口
if (bind(listenFd, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) < 0) {
ESP_LOGE(TAG, "Bind failed");
close(listenFd);
vTaskDelete(NULL);
}

// 监听连接
if (listen(listenFd, 3) < 0) {
ESP_LOGE(TAG, "Listen failed");
close(listenFd);
vTaskDelete(NULL);
}

ESP_LOGI(TAG, "TCP server listening on port %d", PORT);

maxFd = listenFd;
FD_ZERO(&allFdSet);
FD_SET(listenFd,&allFdSet);

while (1) {
readSet = allFdSet;

// 使用select等待活动
nReady = select(maxFd + 1, &readSet, NULL, NULL, NULL);
if ((nReady < 0) && (errno != EINTR)) {
ESP_LOGE(TAG, "Select error");
}

// 新连接请求
//查看是不是监听套接字
if(FD_ISSET(listenFd,&readSet))
{
sinSize = sizeof(struct sockaddr_in);
/* 接受一个客户端连接socket的请求,这个函数调用是阻塞式的 */
int acceptFd = accept(listenFd, (struct sockaddr *)&clientAddr, &sinSize);
/* 返回的是连接成功的socket */
if (acceptFd < 0)
{
ESP_LOGE(TAG,"accept connection failed! errno = %d", errno);
continue;
}
/* 接受返回的client_addr指向了客户端的地址信息 */
ESP_LOGD(TAG,"I get a connection from (%s , %d)",inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port));

FD_SET(acceptFd,&allFdSet);
maxFd = (acceptFd > maxFd) ? acceptFd:maxFd;

// 添加新客户端到数组
for (int i = 0; i < MAX_CLIENTS; i++) {
if (client_socket[i] == 0) {
client_socket[i] = acceptFd;
break;
}
}
}

// 处理已连接客户端的数据
for (int i = 0; i < MAX_CLIENTS; i++)
{
if(client_socket[i] == 0)
{
continue;
}
if(FD_ISSET(client_socket[i],&readSet))
{
int recLen = recv(client_socket[i], buffer, sizeof(buffer), 0);
if(recLen <= 0)
{
ESP_LOGW(TAG,"connection is closed:(%d)",client_socket[i]);
closesocket(client_socket[i]);

FD_CLR(client_socket[i],&allFdSet);
client_socket[i] = 0;
}
else
{
send(client_socket[i],buffer,recLen,0);
}
}
}
}

close(listenFd);
}

void app_main(void)
{
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);

if (CONFIG_LOG_MAXIMUM_LEVEL > CONFIG_LOG_DEFAULT_LEVEL) {
esp_log_level_set("wifi", CONFIG_LOG_MAXIMUM_LEVEL);
}

ESP_LOGI(TAG, "ESP_WIFI_MODE_STA");
wifi_init_sta();

vTaskDelay(3000);
while(1)
{
tcp_server_loop();
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}

二、select关键字详解

select() 是 Unix/Linux 网络编程中用于 I/O 多路复用 的核心函数。它允许程序同时监控多个文件描述符(如套接字),并在其中任意一个或多个描述符就绪(可读、可写或出现异常)时通知程序。这种机制能显著提高程序的并发效率,避免阻塞等待单个描述符。

1、 核心原理

select() 的工作原理基于 阻塞等待 和 描述符集合:

  • 阻塞等待:调用 select() 时,程序会阻塞(暂停执行),直到以下任一情况发生:
    • 至少一个描述符就绪;
    • 超时时间到达;
    • 收到中断信号。
  • 描述符集合:通过 fd_set 结构体管理需要监控的描述符集合,包括:
    • readfds:监控可读事件(如数据到达);
    • writefds:监控可写事件(如缓冲区空闲);
    • exceptfds:监控异常事件(如带外数据)。

2、函数原型

#include <sys/select.h>

int select(
int nfds, // 最大描述符值 + 1
fd_set *readfds, // 监控可读的集合
fd_set *writefds, // 监控可写的集合
fd_set *exceptfds, // 监控异常的集合
struct timeval *timeout // 超时时间
);

  • 返回值:
    • >0:就绪的描述符总数;
    • 0:超时;
    • -1:出错(如 EBADF 无效描述符)。

3、关键操作宏

  • FD_ZERO(fd_set *set):清空集合。
  • FD_SET(int fd, fd_set *set):将描述符 fd 加入集合。
  • FD_CLR(int fd, fd_set *set):将描述符 fd 移出集合。
  • FD_ISSET(int fd, fd_set *set):检查 fd 是否在集合中(就绪时返回真)。

4、 使用步骤

  • 初始化集合:fd_set read_fds;
    FD_ZERO(&read_fds); // 清空集合
    FD_SET(sockfd, &read_fds); // 加入套接字
  • 设置超时(可选):struct timeval tv = {
    .tv_sec = 5, // 5秒
    .tv_usec = 0
    };
  • 调用 select():int max_fd = sockfd + 1;
    int ready = select(max_fd, &read_fds, NULL, NULL, &tv);
  • 检查就绪描述符:if (FD_ISSET(sockfd, &read_fds)) {
    // 处理可读事件
    }
  • 5、示例代码

    以下是一个监控标准输入和套接字的简化示例:

    #include <stdio.h>
    #include <sys/select.h>
    #include <unistd.h>

    int main() {
    fd_set read_fds;
    struct timeval tv;
    int max_fd = STDIN_FILENO + 1; // 标准输入描述符为0

    while (1) {
    FD_ZERO(&read_fds);
    FD_SET(STDIN_FILENO, &read_fds); // 监控标准输入

    tv.tv_sec = 2;
    tv.tv_usec = 0;

    int ready = select(max_fd, &read_fds, NULL, NULL, &tv);
    if (ready < 0) {
    perror("select error");
    break;
    } else if (ready == 0) {
    printf("Timeout, no data.\\n");
    } else {
    if (FD_ISSET(STDIN_FILENO, &read_fds)) {
    char buf[100];
    fgets(buf, sizeof(buf), stdin);
    printf("Input: %s", buf);
    }
    }
    }
    return 0;
    }

    6、优点与局限性

    • 优点:
      • 跨平台兼容性好(Unix/Linux/Windows);
      • 简单易用,适合少量并发连接。
    • 局限性:
      • 描述符数量限制:fd_set 大小固定(通常1024),需通过宏 FD_SETSIZE 扩展;
      • 效率问题:线性扫描所有描述符,高并发时性能下降;
      • 内核态到用户态复制开销:每次调用需传递整个集合。

    7、总结

    select() 是构建轻量级并发服务器的有效工具,尤其适合连接数较少或兼容性要求高的场景。对于高并发需求,可考虑更高效的替代方案(如 epoll 或 kqueue),但 select 的核心思想仍是理解多路复用的基础。

    三、ESP-IDF详解

    1、ESP32 概述

    ESP32 是由乐鑫科技(Espressif Systems)推出的一款高性能、低功耗、高集成度的 Wi-Fi & 蓝牙双模系统级芯片(SoC)。它广泛应用于物联网(IoT)、智能家居、工业控制等领域。其关键特性包括:

    • 双核处理器(通常为 Xtensa LX6,某些型号为 RISC-V)
    • 集成 Wi-Fi (802.11 b/g/n) 和蓝牙 (包括经典蓝牙和低功耗蓝牙 BLE)
    • 丰富的外设接口:

      S

      P

      I

      SPI

      SPI,

      I

      2

      C

      I2C

      I2C,

      I

      2

      S

      I2S

      I2S,

      U

      A

      R

      T

      UART

      UART,

      A

      D

      C

      ADC

      ADC,

      D

      A

      C

      DAC

      DAC,

      P

      W

      M

      PWM

      PWM, 触摸传感器等

    • 充足的内存(RAM 和 Flash 选项多样)
    • 强大的安全特性(如加密加速器)
    • 超低功耗设计,支持多种休眠模式

    2、 ESP-IDF 详解

    ESP-IDF (Espressif IoT Development Framework) 是乐鑫官方为 ESP32、ESP32-S 系列、ESP32-C 系列等芯片提供的开发框架和 SDK(软件开发工具包)。它是开发基于 ESP32 芯片应用程序的首选和官方推荐工具。

    2.1 、ESP-IDF 的核心组件与架构

    ESP-IDF 是一个分层、模块化的框架:

    • 硬件抽象层 (HAL): 提供对芯片硬件资源(如 GPIO、UART、SPI、I2C、定时器、ADC、DAC、RTC、Wi-Fi、蓝牙等)进行操作的统一接口,屏蔽底层硬件细节。
    • 驱动 (Drivers): 在 HAL 之上,提供更易用、功能更丰富的设备驱动(如 SPI Flash 驱动、SD 卡驱动、以太网驱动等)。
    • FreeRTOS: ESP-IDF 深度集成了开源的 FreeRTOS 实时操作系统内核。它提供了任务管理、队列、信号量、软件定时器、中断管理等机制,充分利用 ESP32 的多核特性(任务可以在不同核心上运行)。
    • 中间件:
      • Wi-Fi 协议栈: 实现 Wi-Fi 的 Station(客户端)、SoftAP(接入点)、Promiscuous(监听)模式。
      • 蓝牙协议栈: 实现经典蓝牙和低功耗蓝牙 (BLE) 的各种角色(如 GATT Client/Server, GAP Central/Peripheral)。
      • TCP/IP 协议栈 (lwIP): 轻量级的 TCP/IP 协议栈,支持 IPv4/IPv6 (部分型号)、DHCP、DNS、Socket API 等。
      • 文件系统 (FATFS/VFS): 支持在 SPI Flash 或外部存储设备上使用 FAT 文件系统。
      • 加密库: 提供 AES、SHA、RSA 等加密算法的软件和硬件加速实现。
      • HTTP/WebSocket/MQTT 等协议库: 方便构建网络应用。
    • 应用程序: 用户编写的业务逻辑代码,运行在 FreeRTOS 的任务中,通过调用下层 API 实现功能。

    2.2 、ESP-IDF 开发环境与工具链

    • 工具链: 基于 GNU GCC 编译器(针对 Xtensa 或 RISC-V 架构)。
    • 构建系统: 使用 CMake(早期版本使用 GNU Make)。开发者编写 CMakeLists.txt 文件来定义项目结构、源文件、依赖组件等。
    • 配置工具: menuconfig。这是一个基于文本的图形化配置工具(类似于 Linux Kernel 的 make menuconfig),用于配置 ESP-IDF 的众多选项,如:
      • 选择目标芯片型号
      • 配置 Wi-Fi/BT 参数
      • 调整 FreeRTOS 设置(任务栈大小、优先级等)
      • 配置日志输出级别和方式
      • 配置内存布局
      • 启用/禁用特定功能和外设驱动
    • 核心工具 – idf.py: 这是 ESP-IDF 提供的命令行工具,用于执行构建、烧录、调试、监视串口输出等几乎所有开发任务。常用命令如:
      • idf.py set-target esp32 (设置目标芯片)
      • idf.py menuconfig (启动配置工具)
      • idf.py build (编译项目)
      • idf.py -p PORT flash (烧录固件到设备,PORT 为串口,如 COM3 或 /dev/ttyUSB0)
      • idf.py -p PORT monitor (启动串口监视器,查看设备日志输出)
      • idf.py fullclean (彻底清理构建目录)

    2.3 、ESP-IDF 开发流程简述

  • 环境搭建: 在 Windows、Linux 或 macOS 上安装 ESP-IDF 开发环境(包括工具链、Python、Git 等)。
  • 创建项目: 使用 idf.py create-project 或复制示例项目模板。
  • 编写代码: 在 main 目录下编写应用程序代码 (通常是 main.c 或 app_main() 函数)。
  • 配置项目: 运行 idf.py menuconfig 根据需求进行配置。
  • 编译项目: 运行 idf.py build 生成可执行固件 (.bin 文件)。
  • 烧录固件: 将 ESP32 开发板连接到电脑,运行 idf.py -p PORT flash 将固件烧录到设备 Flash 中。
  • 监视输出: 运行 idf.py -p PORT monitor 查看设备运行日志,进行调试。可以使用 ESP_LOGx (如 ESP_LOGI, ESP_LOGE) 函数打印不同级别的日志。
  • 2.4、 示例代码结构 (最简单的 Hello World)

    #include "freertos/FreeRTOS.h"
    #include "freertos/task.h"
    #include "esp_log.h"

    static const char *TAG = "MAIN";

    void app_main(void)
    {
    while (1) {
    ESP_LOGI(TAG, "Hello World!"); // 打印 Info 级别日志
    vTaskDelay(pdMS_TO_TICKS(1000)); // 延时 1000 毫秒 (FreeRTOS 延时)
    }
    }

    2.5、 优势与特点

    • 官方支持: 由芯片原厂维护,更新及时,与新芯片特性同步快。
    • 功能全面: 提供对 ESP32 所有硬件特性和外设的底层访问。
    • 性能优化: 针对 ESP32 硬件进行了深度优化(如 Wi-Fi/BT 共存)。
    • 稳定性高: 经过大量商业产品验证。
    • 社区活跃: 用户多,社区支持好,问题容易找到解决方案。
    • 文档完善: 官方提供详尽的 API 参考指南、编程指南和示例代码。
    • 模块化: 易于扩展和复用代码,方便管理大型项目。
    • 开源免费: 基于 Apache 2.0 许可证。

    2.6 、适用场景

    ESP-IDF 适用于需要深度控制硬件、追求高性能、高稳定性或需要使用 ESP32 特有功能(如超低功耗协处理器 ULP)的应用开发。对于快速原型开发,也可以考虑基于 ESP-IDF 构建的更高级框架(如 Arduino for ESP32、MicroPython),但这些框架最终都依赖于 ESP-IDF 的底层功能。

    3、总结

    ESP-IDF 是开发 ESP32 系列芯片的强大、灵活且功能完备的官方框架。它提供了从硬件操作到网络协议栈的全套解决方案,结合 FreeRTOS 实现了高效的多任务处理。虽然其学习曲线相对陡峭,尤其是对于不熟悉嵌入式开发和 RTOS 的开发者,但它提供了最直接、最强大的方式来充分利用 ESP32 的潜力,是开发复杂或高性能 ESP32 应用的基石。掌握 idf.py 工具链和 menuconfig 配置是使用 ESP-IDF 的关键。

    在这里插入图片描述

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » ESP32 使用ESP-IDF 连接WiFi并建立TCP服务器(支持多连接)通信源码分享
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!