基于ESP32的TCP服务器

 最近实现了一个无线数据采集模块,可以通过无线方式传输采集到的数据到手机或者PC,免除了连线的烦恼。使用手机作为上位机可以接收数据及发送控制命令,不用带着沉重的PC,在现场调试或者不方便连线的情况下方便快捷。

 模块使用stm32作为主控采集数据,ESP32作为无线模块,芯片间使用SPI交互数据,数据量小可以使用蓝牙BLE/SPP,数据量大使用Wifi,并且ESP32价格便宜使用方便。如果要采集的数据少可以直接使用ESP32作主控,又省掉一颗stm32。本模块的结构如下:

 本文主要介绍ESP32模块这部分,如何启动SoftAP模式以及TCP服务器,如何处理连接断开。

1. 模块选型

 支持Wifi、BT的芯片很多,本模块选用ESP32的原因主要有:

  1. ESP32同时支持Wifi、BT,并且大部分资料都是中文,在物联网领域应用广泛,稳定性有保证。

  2. 性价比高,作为一个20多元人民币的模块,带有双核最高160Mhz主频内核,还有外部Flash和RAM,单纯作为一个MCU使用都是绰绰有余的。

  3. 提供了模组形式,外围电路少,使用方便

  4. 本模块选用了ESP32-WROVER模组, 自带了4MB SPI Flash和8MB PSRAM,可以缓存长时间的采集数据。

 当然这个模块也有一些缺点,比如IO比较少单纯作为主控不太够,编译下载速度比较慢。

 选用ESP32-WROVER模组一个主要原因是自带的8M PSRAM可以缓存长时间的数据,STM32自带的SRAM比较少,而采集的数据大约是5Mbps,缓存在主控端显然是不现实的。而ESP32引出的IO可以使用SPI接口,最高速率10Mbps,可以满足采集数据要求。ESP32官方数据BT SPP速率1.6Mbps,而Wifi可以达到20Mbps,所以只能使用SPI + Wifi的组合。并且实测BT SPP在最高速率下手机端收到的包序不固定,导致难以解析,只有在SPP发送包中间加上至少5ms间隔,才可以让接收和发送的包序一致。

 本模块整体流程为,STM32采集数据,满一包后立即通过SPI发送到ESP32,而ESP32等待数据的Task收到数据后立刻填充到一个大的Ring buffer中,发送数据Task将buffer中数据分包发送出去。实测整个过程数据稳定,上位机解析也没有丢数据的情况。

2. 启动SoftAP模式

 ESP32使用SoftAP模式,这样上位机PC或手机只需要连接到对应的AP,就可以直接通信,省去了Wifi配网的步骤,而且后续TCP通信Server是固定的IP,上位机也比较好实现。稍有不便的是上位机连到ESP32的AP后就无法再通过Wifi连接外网。

Note:本文代码都基于esp-idf-v4.3

 SoftAP参考了IDF的"examples\wifi\getting_started\softAP",改动了几个地方。首先SSID根据本身mac变化,这样多个模块在一起可以根据SSID区分,代码如下:

#define ESP_WIFI_SSID      "ESP32"
#define ESP_WIFI_PASS      "12345678"

void Esp_WifiApInit(void)
{
    ......

    uint8_t ApMac[6];

    esp_wifi_get_mac(ESP_IF_WIFI_AP, ApMac);
    sprintf((char *)wifi_config.ap.ssid , "%s_%02X%02X" , ESP_WIFI_SSID , 
                                           ApMac[4] , ApMac[5]);
    wifi_config.ap.ssid_len = strlen(ESP_WIFI_SSID) + 5;

    ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &wifi_config));
}

 另一个修改和TCP服务端有关,在Wifi station连接后建立IP和MAC地址的映射表,目的是station断开Wifi连接后关闭socket。因为测试中发现,如果在连接socket的情况下,直接断开Wifi连接,socket不会立刻断开。网络上找到的查询IP和MAC映射都是ARP协议,但是没有找到ESP32如何使用,所以就自行实现了一个简单的映射表,主要利用分配IP的Event,将IP和MAC保存到一组表中。主要代码为:

typedef struct
{
    uint32_t ip;        //IPv4
    uint8_t mac[6];
    uint8_t connect;
} AP_TaskDef;

AP_TaskDef APInfo[MAX_STA_CONN];
int8_t APIndex = -1;

/*加入IP*/
int32_t Esp_PutAPClientIP(uint32_t addr) 
{
    if (APIndex < 0)
    {
        return -1;
    }
    APInfo[APIndex].ip = addr;
    APIndex = -1;
    ESP_LOGI(WIFI_TAG, "Put AP:%d", addr);
    return 0;
}

/*加入MAC*/
int32_t Esp_PutAPClinetMAC(uint8_t* mac)
{
    for (size_t i = 0; i < MAX_STA_CONN; i++)
    {
        if (APInfo[i].connect == 0)
        {
            memcpy(APInfo[i].mac , mac, 6);
            APInfo[i].connect = 1;
            APIndex = i;
            return 0;
        }
    }

    return -1;
}

/*移除IP*/
__attribute__((__weak__)) int32_t Esp_PopClientIP(uint32_t addr)
{
    return 0;
}

/*移除MAC*/
int32_t Esp_PopAPClinetMAC(uint8_t* mac)
{
    for (size_t i = 0; i < MAX_STA_CONN; i++)
    {
        if (memcmp(mac, APInfo[i].mac, 6) == 0)
        {
            if (APInfo[i].connect == 1)
            {
                APInfo[i].connect = 0;
                Esp_PopClientIP(APInfo[i].ip);
                ESP_LOGI(WIFI_TAG, "Pop AP:%d", APInfo[i].ip);
                APInfo[i].ip = 0;
            }
            return 0;
        }
    }
    return -1;
}

static void wifi_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) {
            wifi_event_ap_staconnected_t* event = (wifi_event_ap_staconnected_t*) event_data;
            ESP_LOGI(WIFI_TAG, "station "MACSTR" join, AID=%d",
                 MAC2STR(event->mac), event->aid);
            Esp_PutAPClinetMAC(event->mac);
        } else if (event_id == WIFI_EVENT_AP_STADISCONNECTED) {
            wifi_event_ap_stadisconnected_t* event = (wifi_event_ap_stadisconnected_t*) event_data;
            ESP_LOGI(WIFI_TAG, "station "MACSTR" leave, AID=%d",
                    MAC2STR(event->mac), event->aid);
            Esp_PopAPClinetMAC(event->mac);
        }
    } 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(WIFI_TAG, "IP assigned");
            // Add to table
            Esp_PutAPClientIP(event->ip.addr);
        }
    }
}

void Esp_WifiApInit(void)
{
    ......
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));

    ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,
                                                        ESP_EVENT_ANY_ID,
                                                        &wifi_event_handler,
                                                        NULL,
                                                        NULL));
    /*添加IP分配的Event*/
    ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT,
                                                        IP_EVENT_AP_STAIPASSIGNED,
                                                        &wifi_event_handler,
                                                        NULL,
                                                        NULL));
    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP));
    ......
}

Note:自行建立映射的方法比较麻烦,也许有更好的办法

 连接断开Wifi的log如下:

3. 创建TCP服务端

 TCP server参考了IDF的"examples\protocols\sockets\tcp_server",ESP32创建TCP server并等待客户端连接,需要修改支持多客户端,并且数据将可以同时发送到多个客户端。

 启动TCP server相关任务,Esp_TCPServerTask初始化并等待socket连接,Esp_TCPSendTask用于从数据buffer中取数据并发送到TCP客户端:

int32_t Esp_TCPServerStart(void)
{
    ......
    ret = xTaskCreate(Esp_TCPServerTask, 
                      TCP_TAG, 
                      4096,
                      NULL, 
                      tskIDLE_PRIORITY + 1,
                      &TCPTaskHandle);
    if (ret != pdPASS)
    {
        ESP_LOGE(TCP_TAG, "Task create error");
        return ret;
    }
    ESP_LOGI(TCP_TAG, "Create RW task");
    ret = xTaskCreate(Esp_TCPSendTask, 
                      "Send", 
                      4096,
                      NULL, 
                      tskIDLE_PRIORITY + 1,
                      &TCPSendHandle);
    if (ret != pdPASS)
    {
        ESP_LOGE(TCP_TAG, "Task create error");
        return ret;
    }
    return 0;
}

Esp_TCPServerTask获取到一个连接客户端后,将客户端socket和IP地址放到客户端列表,同时新建Task接收数据。客户端断开后recv接口返回错误,服务器端也将关闭对应socket,并移除客户端列表。

void Esp_TCPRecvTask(void * pvParameters)
{
    ......
    for(;;)
    {
        len = recv(sock, rx_buffer, sizeof(rx_buffer), 0);
        if (len < 0) {
            ESP_LOGE(TCP_TAG, "Error occurred during receiving: errno %d", errno);
            Esp_PopClient(sock);
            break;
        } else if (len == 0) {
            ESP_LOGW(TCP_TAG, "Connection closed");
            Esp_PopClient(sock);
            break;
        } else {
        #if TCP_DBG
            ESP_LOGI(TCP_TAG, "Recv:%d", len);
            esp_log_buffer_hex(TCP_TAG, rx_buffer, len);
        #endif
        }
    }
    vTaskDelete(NULL);
}

void Esp_TCPServerTask(void * pvParameters)
{
    ......
    ESP_ERROR_CHECK(Esp_TCPServerInit(&serverSocket));  //初始化socket,绑定端口
    for(;;)
    {
        clientSocket = Esp_TCPServerAccept(serverSocket, &ip_addr); //等待客户端连接
        if (clientSocket < 0)
        {
            ESP_LOGE(TCP_TAG, "Unable to accept");
        } else {
            if (Esp_PutClient(clientSocket, ip_addr) == 0)  //添加到客户端列表
            {
                int32_t ret;
                ret = xTaskCreate(Esp_TCPRecvTask,          //为客户端创建Task接收数据
                                    "Recv",
                                    4096,
                                    (void*)clientSocket,    //socket作为任务参数
                                    tskIDLE_PRIORITY + 1,
                                    NULL);
                if (ret != pdPASS)
                {
                    ESP_LOGE(TCP_TAG, "Task create error");
                }
            } else {
                ESP_LOGE(TCP_TAG, "Too many clients");
            }
        }
    }
}

 严格说由于IDF基于FreeRtos,添加、移除客户端列表需要放到临界区,实测中客户端数目不多,并且不会频繁连接断开,并没有出现异常。TCP客户端连接,发送数据,断开过程如下:

4.总结

 在一些连线不便的情况下,使用ESP32 TCP方式传输数据速率快,通信稳定,十分实用。用Wifi传输功耗比较高,如果用电池供电,需要注意电量。后续再介绍模块的其他部分,如SPI通信、Hanshake方法、Ring buffer使用等。

Github: https://github.com/songdaw/esp32_tcp_server
码云:https://gitee.com/songdaw/esp32_tcp_server

  • 4
    点赞
  • 44
    收藏
    觉得还不错? 一键收藏
  • 9
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值