最近实现了一个无线数据采集模块,可以通过无线方式传输采集到的数据到手机或者PC,免除了连线的烦恼。使用手机作为上位机可以接收数据及发送控制命令,不用带着沉重的PC,在现场调试或者不方便连线的情况下方便快捷。
模块使用stm32作为主控采集数据,ESP32作为无线模块,芯片间使用SPI交互数据,数据量小可以使用蓝牙BLE/SPP,数据量大使用Wifi,并且ESP32价格便宜使用方便。如果要采集的数据少可以直接使用ESP32作主控,又省掉一颗stm32。本模块的结构如下:
本文主要介绍ESP32模块这部分,如何启动SoftAP模式以及TCP服务器,如何处理连接断开。
1. 模块选型
支持Wifi、BT的芯片很多,本模块选用ESP32的原因主要有:
-
ESP32同时支持Wifi、BT,并且大部分资料都是中文,在物联网领域应用广泛,稳定性有保证。
-
性价比高,作为一个20多元人民币的模块,带有双核最高160Mhz主频内核,还有外部Flash和RAM,单纯作为一个MCU使用都是绰绰有余的。
-
提供了模组形式,外围电路少,使用方便
-
本模块选用了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