最近一直在做关于气浮台的项目,里面有一个小环节就是需要把设备的数据传输下来,因为之前对通信几乎是小白,加上时间比较紧,凡是涉及到底层的东西都不敢碰,最后比较了一番选了ESP 8266这个模块来开发,通过AT指令进行开发,用的是C语言,运行在PC 104上(当然普通PC更没问题了),大概五天时间就做完了,下面介绍一下详细内容。
(一)ESP 8266模块介绍
这个模块的详细资料网上很容易找到,在此就不详述了,简单说几点吧。
这个模块开发有两种方式:第一种是用官方SDK来开发,适合对硬件有一定了解的朋友入手,因为这个模块本身的功能其实很强大,只用来通讯有点小题大做的感觉,但是这种方式不适合新手,入手难度有点高;第二种就是AT指令开发,很简单,拿一般的串口助手就可以调试。(注意调试的时候一定要先按回车再发送)
这个模块总共有三种工作方式:AP,STATION,AP+STATION。因为我需要完成的是多个设备数据传输,因此透传就不考虑了,这里我用的是一个模块用作热点同时开启服务器(用AP+STATION),通过串口接在终端上收数据;其他的模块通过串口接在设备上(用STATION)。相当于组建了一个小的局域网,基于TCP协议的WiFi通信。
这里再单独提一下,用AT指令开发有一个很头疼的地方在于指令的返回格式不统一,所以程序里面的判断条件会比较多。后面我会仔细的总结一下,其他的信息大家可以去找用户手册,里面对模块的介绍以及AT指令都比较完整。
(二)用C语言实现WIN 32下的串口通讯
这一步说白了就是怎么用C语言去完成串口助手最基本的功能,但是也必须要仔细,很多地方容易出错。
1. 首先打开串口,Createfile函数的具体用法在此不详述了,不熟悉的朋友可以去百度。
espCom = CreateFile("COM3", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL); if (espCom == INVALID_HANDLE_VALUE) { printf("open COM3 failed\n"); exit(2); }
提醒一下大家,如果设备的串口不是COM1-COM9,比如是COM10,那么函数第一个不能写成“COM10”了,得写成“\\\\.\\COM10”,因为COM10以上的串口对于文件名系统而言只是一般的文件,而非串行设备。
2. 完成串口相应的配置工作
espTimeOuts.ReadIntervalTimeout = 500;//MAXDWORD; //5000; espTimeOuts.ReadTotalTimeoutConstant = 5000; //0;//1000; espTimeOuts.ReadTotalTimeoutMultiplier = 500;// 0;//500; espTimeOuts.WriteTotalTimeoutConstant = 2000; espTimeOuts.WriteTotalTimeoutMultiplier = 500; if (!SetCommTimeouts(espCom, &espTimeOuts)) { printf("写入超时参数错误\n"); exit(3); } if (!SetupComm(espCom, 1024, 1024)) { printf("设置串口读写缓冲区失败\n"); exit(4); } if (!GetCommState(espCom, &espdcb)) { printf("获取串口属性失败\n"); exit(5); } espdcb.BaudRate = BAUD_RATE; espdcb.ByteSize = 8; espdcb.Parity = NOPARITY; espdcb.StopBits = ONESTOPBIT; if (!SetCommState(espCom, &espdcb)) { printf("设置串口参数出错\n"); exit(6); } printf("无线通信串口打开成功!\n"); PurgeComm(espCom, PURGE_TXCLEAR | PURGE_RXCLEAR | PURGE_RXABORT | PURGE_TXABORT); //清空缓冲区
里面总共涉及到了5个工作,对应的函数介绍不理解请自行百度,代码只简单地注释了一下。其中容易出问题的在于SetCommTimeouts的时间设置,读取时间间隔、延时这些大家一定要仔细,初始参数可以就按上面的取,但如果出现乱码或者读取不完整的情况,先调一调这几个参数,每个人的需求不同,以上参数也可能不同。
3. 接下来分别建立读和写的线程
为了方便后面的介绍,这里先把一些代码定义贴出来(以下介绍将分为服务器和客户端两部分)
(1)这是客户端的定义
HANDLE espCom; COMMTIMEOUTS espTimeOuts; COMSTAT comstat; DCB espdcb; unsigned int esp_order = 0; //指令执行顺序 char ESP_RXBUFF[200]; char ESP_SENDDATA[200] = {}; //数据发送包 float ESP_SYS_DATA[20] = {}; //系统数据 BOOL ESP_RTS=0; //数据发送请求标志 //AT指令集 const char *esp_com_AT = { "AT\r\n" }; //test const char *esp_com_AT_CWMODE = { "AT+CWMODE=1\r\n" }; //Station模式 const char *esp_com_AT_CIPMUX = { "AT+CIPMUX=1\r\n" }; //多连接模式 const char *esp_com_AT_CIPSERVER = { "AT+CIPSERVER=0\r\n" }; //关闭服务器 const char *esp_com_AT_CWJAP = { "AT+CWJAP=\"esp\",\"123456\"\r\n" }; //连接esp网络 const char *esp_com_AT_CIPSTART = { "AT+CIPSTART=\"TCP\",\"192.168.4.1\",5000\r\n" }; //加入服务器 const char *esp_com_AT_CIPSEND = { "AT+CIPSEND=40\r\n" }; //请求发送数据 const char *esp_com_AT_RST = { "AT+RST\r\n" }; //ESP8266重启 int esp_AT_len = strlen(esp_com_AT); int esp_AT_CWMODE_len = strlen(esp_com_AT_CWMODE); int esp_AT_CIPMUX_len = strlen(esp_com_AT_CIPMUX); int esp_AT_CIPSERVER_len = strlen(esp_com_AT_CIPSERVER); int esp_AT_CWJAP_len = strlen(esp_com_AT_CWJAP); int esp_AT_CIPSTART_len = strlen(esp_com_AT_CIPSTART); int esp_AT_CIPSEND_len = strlen(esp_com_AT_CIPSEND); int esp_AT_RST_len = strlen(esp_com_AT_RST);
主函数中开启读写线程
HANDLE hThread1 = CreateThread(NULL, 0, ReadThread, 0, 0, NULL); //读线程 HANDLE hThread2 = CreateThread(NULL, 0, WriteThread, 0, 0, NULL); //写线程 CloseHandle(hThread1); CloseHandle(hThread2);
函数具体内容。总的来说就是依靠esp_order来一条一条发送指令,在确保指令执行后再发送下一条。(在这里希望大家把需要用到的指令都在串口助手上试一遍,特别是返回的内容一定要看清楚,大部分指令都是返回XXXXXXXXXXX OK,这一部分只需要检测OK就能确保指令执行了,但有些特殊的就需要单独设置了,比如重启指令最后会返回一串乱码+ready)
int ESP_ReceiveChar() //读命令 { DWORD ESP_READ_COUNT; BOOL bReadResult; BOOL bResult; DWORD dwError; for (;;) { bResult = ClearCommError(espCom, &dwError, &comstat); if (comstat.cbInQue == 0) continue; bReadResult = ReadFile(espCom, ESP_RXBUFF, 200, &ESP_READ_COUNT, NULL); if (!bReadResult) { printf("读串口失败!\n"); return FALSE; } if ((esp_order == 0)||(esp_order>=4)) //检查测试指令以及后续指令的返回情况 { if (ESP_RXBUFF[ESP_READ_COUNT - 4] == 'O') { printf("%s\r", ESP_RXBUFF); esp_order++; } } else { if (ESP_RXBUFF[ESP_READ_COUNT - 4] == 'd') //重启指令检查 { printf("ready\n"); esp_order++; } } if ((ESP_RXBUFF[ESP_READ_COUNT - 4] == 'I') || (ESP_RXBUFF[ESP_READ_COUNT - 5] == 'T')) //检查自动连接WiFi的情况 { printf("%s\r", ESP_RXBUFF); esp_order++; } if (ESP_RXBUFF[ESP_READ_COUNT - 2] == '>') //数据发送请求成功标志 { printf("%s", ESP_RXBUFF); esp_order++; } memset(ESP_RXBUFF, 0, 200);//清空缓冲区 PurgeComm(espCom, PURGE_RXCLEAR | PURGE_RXABORT); } return 0; } DWORD WINAPI ReadThread(LPVOID pParam) //读线程 { ESP_ReceiveChar(); return 0; } int ESP_WriteChar(const char* WriteBuffer, DWORD NumToSend) //写命令 { COMSTAT ComStat; DWORD dwErrorFlags; BOOL bWriteStat; DWORD BytesSent; ClearCommError(espCom, &dwErrorFlags, &ComStat); bWriteStat = WriteFile(espCom, WriteBuffer, NumToSend, &BytesSent, NULL); if (!bWriteStat) printf("写串口失败"); if (BytesSent != NumToSend) printf("WARNING: WriteFile() error.. Bytes Sent: %d; MessageLength: %d\n", BytesSent, NumToSend); PurgeComm(espCom, PURGE_TXCLEAR | PURGE_TXABORT); return true; } DWORD WINAPI WriteThread(LPVOID pParam) //写线程 { while (espCom != INVALID_HANDLE_VALUE) { Sleep(1000); if (esp_order == 0) ESP_WriteChar(esp_com_AT, esp_AT_len); if (esp_order == 1) ESP_WriteChar(esp_com_AT_RST, esp_AT_RST_len); if (esp_order == 4) ESP_WriteChar(esp_com_AT_CIPSTART, esp_AT_CIPSTART_len); if ((esp_order >= 5) && (esp_order % 2 != 0) && (ESP_RTS == 0)) //发送数据请求 { ESP_RTS = 1; ESP_WriteChar(esp_com_AT_CIPSEND, esp_AT_CIPSEND_len); //Sleep(500); //ESP_WriteChar(ESP_SENDDATA_TEST, strlen(ESP_SENDDATA_TEST)); } if ((esp_order >= 5) && (esp_order % 2 == 0) && (ESP_RTS == 1)) //数据发送 { ESP_RTS = 0; memset(ESP_SENDDATA, 0, 200); unsigned char esp_ls[4]; for (int i = 0, j = 0; i < 10; i++, j += 4) { memcpy(esp_ls, &ESP_SYS_DATA[i], sizeof(float)); ESP_SENDDATA[j] = esp_ls[0]; ESP_SENDDATA[j + 1] = esp_ls[1]; ESP_SENDDATA[j + 2] = esp_ls[2]; ESP_SENDDATA[j + 3] = esp_ls[3]; } //检查发送数据 float final[10]; for (int i = 0, j = 0; i < 10; i++, j += 4) { memcpy(&final[i], &ESP_SENDDATA[j], sizeof(float)); } for (int i = 0; i<10; i++) printf("%f ", final[i]); printf("\n"); ESP_WriteChar(ESP_SENDDATA, 40); } } return true; }
!!!注意:以上代码执行的前提是先按以下指令在串口助手进行设置
AT+CWMODE=1
AT+CIPMUX=1
AT+CIPSERVER=0
AT+CWJAP="esp","123456" //这个是默认的WiFi,也可以自己设置名字密码
AT+CIPSTART="TCP","192.168.4.1",5000 //ip地址需要在服务器那个模块上进行查询(后面关于服务器的代码有),5000是开服务器的时候设置的端口
(2)服务器的定义
HANDLE espCom; COMMTIMEOUTS espTimeOuts; COMSTAT comstat; DCB espdcb; unsigned int esp_order = 0; //指令执行顺序 char ESP_RXBUFF[200]; //读入数据缓冲区 //AT指令集 const char *esp_com_AT = { "AT\r\n" }; //TEST const char *esp_com_AT_CWMODE = { "AT+CWMODE=3\r\n" }; //AP+Station const char *esp_com_AT_CIPMUX = { "AT+CIPMUX=1\r\n" }; //多连接模式 const char *esp_com_AT_CIPSERVER = { "AT+CIPSERVER=1,5000\r\n" }; //开启服务器 const char *esp_com_AT_CIFSR = { "AT+CIFSR\r\n" }; //查询IP地址 int esp_AT_len = strlen(esp_com_AT); int esp_AT_CWMODE_len = strlen(esp_com_AT_CWMODE); int esp_AT_CIPMUX_len = strlen(esp_com_AT_CIPMUX); int esp_AT_CIPSERVER_len = strlen(esp_com_AT_CIPSERVER); int esp_AT_CIFSR_len = strlen(esp_com_AT_CIFSR);
读写线程的开启与客户端相同,下面贴出具体函数
int ESP_ReceiveChar() //读指令 { DWORD ESP_READ_COUNT; BOOL bReadResult; BOOL bResult; DWORD dwError; for (;;) { bResult = ClearCommError(espCom, &dwError, &comstat); //清除串口error if (comstat.cbInQue == 0) continue; bReadResult = ReadFile(espCom, ESP_RXBUFF, 200, &ESP_READ_COUNT, NULL); if (!bReadResult) { printf("读串口失败!\n"); return FALSE; } if (esp_order == 6) //注意两个if调用顺序 { if (ESP_RXBUFF[ESP_READ_COUNT - 5] == 'E') //显示client连接情况 printf("%s\n", ESP_RXBUFF); else //client发送的数据 { //printf("原始:%s\n", ESP_RXBUFF); float final[10]; for (int i = 0; i <= 11; i++) //ESP_RXBUFF头字节 { printf("%c", ESP_RXBUFF[i]); } printf("\n"); for (int i = 0, j = 12; i < 10; i++, j += 4) //数据包 { memcpy(&final[i], &ESP_RXBUFF[j], sizeof(float)); } for (int i = 0; i < 10; i++) printf("%f ", final[i]); printf("\n"); } } if (ESP_RXBUFF[ESP_READ_COUNT - 4] == 'O') //指令执行情况 { printf("%s\r", ESP_RXBUFF); esp_order++; //printf("%d\n",strlen(ESP_RXBUFF));//AT指令下返回字符串长度为11 } memset(ESP_RXBUFF, 0, 200);//清空缓冲区 //printf("缓冲区长度:%d\n", strlen(ESP_RXBUFF)); PurgeComm(espCom, PURGE_RXCLEAR | PURGE_RXABORT); } return 0; } DWORD WINAPI ReadThread(LPVOID pParam) //读线程 { ESP_ReceiveChar(); return 0; } int ESP_WriteChar(const char* WriteBuffer, DWORD NumToSend) //写指令 { COMSTAT ComStat; DWORD dwErrorFlags; BOOL bWriteStat; DWORD BytesSent; ClearCommError(espCom, &dwErrorFlags, &ComStat); bWriteStat = WriteFile(espCom, WriteBuffer, NumToSend, &BytesSent, NULL); if (!bWriteStat) { printf("写串口失败"); } if (BytesSent != NumToSend) { printf("WARNING: WriteFile() error.. Bytes Sent: %d; MessageLength: %d\n", BytesSent, NumToSend); } PurgeComm(espCom, PURGE_TXCLEAR | PURGE_TXABORT); return true; } DWORD WINAPI WriteThread(LPVOID pParam) //写线程 { while (espCom != INVALID_HANDLE_VALUE) { Sleep(1000); if (esp_order == 0) ESP_WriteChar(esp_com_AT, esp_AT_len); else if (esp_order == 1) ESP_WriteChar(esp_com_AT_CWMODE, esp_AT_CWMODE_len); else if (esp_order == 2) ESP_WriteChar(esp_com_AT_CIPMUX, esp_AT_CIPMUX_len); else if (esp_order == 3) ESP_WriteChar(esp_com_AT_CIPSERVER, esp_AT_CIPSERVER_len); else if (esp_order == 4) { esp_order++; ESP_WriteChar(esp_com_AT_CIFSR, esp_AT_CIFSR_len); } } return true; }
代码有点绕,简单解释一下吧。先按照所需的AT指令进行设置,然后就是接收客户端发来的数据,我上面的发送内容是设备数据,都是float类型,通过memcpy换到char数组然后发送出去。可以对照客户端的代码看一下。
这是实际测试效果
(三)总结
ESP 8266是一个很容易上手的无线通信模块,在物联网领域用的很多。用AT指令可以加快开发速度,但是如果对传输要求较高,时间比较充裕也可以去采用SDK,效果会更好。上面的代码又不太明白的朋友可以留言或者私信,有空我会尽可能帮助大家。