最近工作上遇到一个项目,客户之前使用的是2G模块,用于检测三相电流和各相的温度并通过模块联网上传到服务器。该服务器与模块使用TCP直连,私有协议。目前通知2G网络逐步开始停止维护了,所以需要将2G模块升级成4G模块。之前做过一个使用合宙4G模块和阿里云平台通过MQTT通讯的案子,所以该项目就用这套方案将通讯这部分改一改就能用了。
这次主要是讲一下TCP通讯所需的相关硬件,其他的一些诸如采集数据等电路有空再说吧~




采用中微的BAT32G137,M0核,该芯片内置128K Flash和12K SRAM。其他如12Bit AD转换口,串口,定时器都是常规配置,比较齐全,封装LQFP32,脚位也够用。
合宙Air724ug,支持AT指令。带网络相关服务,支持http,tcp,mqtt。支持文件系统读写,电话本命令,短消息命令,通话和音频相关。总之是一个比较强大的模组。具体可以参考合宙官网

这边主要是注意一下MCU和合宙模块电压匹配问题,MCU使用3.3V供电,所以IO输出也是3.3V。而模块输入电压4.16V,经过内部转换之后输出1.8V。这里就分享一个常用的转换电路。简单说一下原理:电路采用一个三极管,一个二极管,3个电阻组成。左边为模块串口,右边则是MCU的串口。

首先看一下规格书上给出的这份流程图(见下图:TCP流程)。模块上电部分,主要通过MOS和三极管组合来通断电源给模块供电,并且Powerkey脚硬件上直接接地。之后就是一系列的AT指令,在写程序前可以先使用模块带的usb口通过上位机软件先测试一下。

AT+RESET\r\n // 重启模块,一般不从这一步开始
AT\r\n // 测试是否开机
ATE0\r\n // 关闭回显
AT+GSN\r\n // 获取IEMI,用于系统唯一性认证
AT+CPIN?\r\n // 获取SIM状态
AT+ICCID\r\n // 获取SIM卡号,可以上传给服务器
AT+CSQ\r\n // 获取信号强度
AT+CGREG?\r\n // GPRS注册状态
AT+CGATT?\r\n // 是否附着GPRS
AT+CIPMUX=0\r\n // 开单路连接,这个看情况是否需要
AT+CIPQSEND=1\r\n // 设置快发模式
AT+CSTT\r\n // 设置APN
AT+CIICR\r\n // 激活移动场景
AT+CIFSR\r\n // 查询IP,到这里设备初始化完成,后面就需要TCP连接
AT+CIPSTART="TCP",IP,PORT\r\n // 链接服务器 OK CONNECT OK
AT+CIPSTATUS\r\n // 查询下链接状态 STATE: CONNECT OK
AT+CIPSEND=<length> // 发送数据,返回”>“ 表示可以发送具体的数据,数据长度和length相同
AT+CIPCLOSE // 关闭TCP连接,单路直接发送
// 保活,两种方式,前一种是TCP底层,后一种心跳报是应用层,需要加在AT+CIPMUX前
AT+CIPTKA=<mode>[,<keepIdle>[,<keepInterval>[,<keepCount>]]
AT^HEARTCONFIG=<option>,<socket_id>,<heartbeat_time>以上就是使用到的全部AT指令,前面9步是模块基础的初始化部分,10开始是TCP相关指令。详细指令参数说明可以参考Air724UG模块AT指令手册。
着重说一下这些指令中的保活,AT指令有两种保活方式。分为协议层面和应用层面。我分别说一下这两种方式的区别:
AT+CIPTKA=<mode>[,<keepIdle>[,<keepInterval>[,<keepCount>]] // 保活
AT+CIPTKA=1,120,75,9开启保活,120秒时间没有数据交互则每75秒发送一个保活探针,一共9次AT^HEARTCONFIG=<option>,<socket_id>,<heartbeat_time> // 心跳包
AT^HEARTCONFIG=1,0,120开启发送心跳,每120秒发送一个心跳包两种方式中客户端推荐使用方式1,服务端推荐方式2。
[2025-07-24 15:16:18.787] SEND >>>>>>>>>> AT
[2025-07-24 15:16:18.787]
[2025-07-24 15:16:18.804] AT
[2025-07-24 15:16:18.804]
[2025-07-24 15:16:18.804] OK
[2025-07-24 15:16:18.804]
[2025-07-24 15:18:52.743] SEND >>>>>>>>>> ATE0
[2025-07-24 15:18:52.743]
[2025-07-24 15:18:52.766] ATE0
[2025-07-24 15:18:52.766]
[2025-07-24 15:18:52.766] OK
[2025-07-24 15:18:52.766]
[2025-07-24 15:19:12.617] SEND >>>>>>>>>> AT+GSN
[2025-07-24 15:19:12.617]
[2025-07-24 15:19:12.620]
[2025-07-24 15:19:12.620] 861658062303204
[2025-07-24 15:19:12.620]
[2025-07-24 15:19:12.620] OK
[2025-07-24 15:19:12.620]
[2025-07-24 15:19:20.467] SEND >>>>>>>>>> AT+CPIN?
[2025-07-24 15:19:20.467]
[2025-07-24 15:19:20.495]
[2025-07-24 15:19:20.495] +CPIN: READY
[2025-07-24 15:19:20.495]
[2025-07-24 15:19:20.495] OK
[2025-07-24 15:19:20.495]
[2025-07-24 15:19:33.093] SEND >>>>>>>>>> AT+ICCID
[2025-07-24 15:19:33.093]
[2025-07-24 15:19:33.109]
[2025-07-24 15:19:33.109] +ICCID: 89861501112441093935
[2025-07-24 15:19:33.109]
[2025-07-24 15:19:33.109] OK
[2025-07-24 15:19:33.109]
[2025-07-24 15:20:10.767] SEND >>>>>>>>>> AT+CSQ
[2025-07-24 15:20:10.767]
[2025-07-24 15:20:10.783]
[2025-07-24 15:20:10.783] +CSQ: 22,99
[2025-07-24 15:20:10.783]
[2025-07-24 15:20:10.783] OK
[2025-07-24 15:20:10.783]
[2025-07-24 15:20:15.500] SEND >>>>>>>>>> AT+CGREG?
[2025-07-24 15:20:15.500]
[2025-07-24 15:20:15.502]
[2025-07-24 15:20:15.502] +CGREG: 0,1
[2025-07-24 15:20:15.502]
[2025-07-24 15:20:15.502] OK
[2025-07-24 15:20:15.502]
[2025-07-24 15:20:20.159] SEND >>>>>>>>>> AT+CGATT?
[2025-07-24 15:20:20.159]
[2025-07-24 15:20:20.183]
[2025-07-24 15:20:20.183] +CGATT: 1
[2025-07-24 15:20:20.183]
[2025-07-24 15:20:20.183] OK
[2025-07-24 15:20:20.183]
[2025-07-24 15:20:25.925] SEND >>>>>>>>>> AT+CIPMUX=0
[2025-07-24 15:20:25.925]
[2025-07-24 15:20:25.926]
[2025-07-24 15:20:25.926] OK
[2025-07-24 15:20:25.926]
[2025-07-24 15:20:31.700] SEND >>>>>>>>>> AT+CIPTKA=1,120,75,9
[2025-07-24 15:20:31.700]
[2025-07-24 15:20:31.702]
[2025-07-24 15:20:31.702] OK
[2025-07-24 15:20:31.702]
[2025-07-24 15:20:40.109] SEND >>>>>>>>>> AT+CIPQSEND=1
[2025-07-24 15:20:40.109]
[2025-07-24 15:20:40.111]
[2025-07-24 15:20:40.111] OK
[2025-07-24 15:20:40.111]
[2025-07-24 15:20:45.660] SEND >>>>>>>>>> AT+CSTT
[2025-07-24 15:20:45.660]
[2025-07-24 15:20:45.660]
[2025-07-24 15:20:45.660] OK
[2025-07-24 15:20:45.660]
[2025-07-24 15:20:50.124] SEND >>>>>>>>>> AT+CIICR
[2025-07-24 15:20:50.124]
[2025-07-24 15:20:50.127]
[2025-07-24 15:20:50.127] OK
[2025-07-24 15:20:50.127]
[2025-07-24 15:20:54.391] SEND >>>>>>>>>> AT+CIFSR
[2025-07-24 15:20:54.391]
[2025-07-24 15:20:54.393]
[2025-07-24 15:20:54.393] 10.98.97.7
[2025-07-24 15:20:54.393]
[2025-07-24 15:21:02.966] SEND >>>>>>>>>> AT+CIPSTATUS
[2025-07-24 15:21:02.966]
[2025-07-24 15:21:02.980]
[2025-07-24 15:21:02.980] OK
[2025-07-24 15:21:02.980]
[2025-07-24 15:21:02.980] STATE: IP STATUS
[2025-07-24 15:21:02.980]
[2025-07-24 15:21:54.265] SEND >>>>>>>>>> AT+CIPSTART="TCP",112.125.89.8,42898
[2025-07-24 15:21:54.265]
[2025-07-24 15:21:54.271]
[2025-07-24 15:21:54.271] OK
[2025-07-24 15:21:54.271]
[2025-07-24 15:21:54.457]
[2025-07-24 15:21:54.457] CONNECT OK
[2025-07-24 15:21:54.457]
[2025-07-24 15:23:04.223] SEND >>>>>>>>>> AT+CIPSEND=10
[2025-07-24 15:23:04.223]
[2025-07-24 15:23:04.225]
[2025-07-24 15:23:04.225] >
[2025-07-24 15:23:14.639] SEND >>>>>>>>>> HelloWorld
[2025-07-24 15:23:14.639]
[2025-07-24 15:23:14.642]
[2025-07-24 15:23:14.642] DATA ACCEPT:10 以上是使用上位机串口工具和模块通讯初始化的过程,其中【SEND >>>>>>>>>>】是上位机发送的数据,其他为模块回传数据,空行表示“\r\n”,也就是换行符。有了上述过程,那么程序部分就很容易了。另外提一嘴,4G模块和WIFI模块不一样,不能使用局域网调试TCP,需要配置公网环境,这个比较麻烦。这里推荐一个网站可以开启公网TCP端口用于调试,TCP在线调试网站。
先来看看设备初始化相关部分代码
WORK_EXT void ConfigModuleNoBlack(void);
WORK_EXT void (*ConfigConnectDispose)(char* dat); // 处理函数的指针,放在while循环中
WORK_EXT void SendConfigFunction(const char* order, void (*functionSent)(void), char* hopeReturn1, char* hopeReturn2, void (*functionPares)(char* dat), u16 nextDelay);
WORK_EXT void GetDeviceIP(char* dat);
WORK_EXT void GetDeviceInfo(char* dat);
WORK_EXT void GetSimInfo(char* dat);
WORK_EXT void GetSignal(char* dat);
WORK_EXT void GetConnectState(char* dat);
WORK_EXT void ConfigModuleDataHandle(char* dat);
WORK_EXT void ConfigModuleRunNext(u16 nextTime);从下往上看,是处理不同回复的函数,最后在ConfigModuleNoBlack函数中进行各个功能使用。
/*----------------------------------------------------------------
*Function: ConfigModuleRunNext
*Description: 设置下一条指令运行时间
*Input: none
*Output: none
*Return: none
*Others: none
//----------------------------------------------------------------*/
void ConfigModuleRunNext(u16 nextTime)
{
gbDataReturnFlag = 1;
//gsCountTime.sendNextDelay = nextTime;
gsCountTime.configModuleCnt = nextTime;
}搭配下面几个函数中使用,表示收到有效的回复,进行下一步
/*----------------------------------------------------------------
*Function: ConfigModuleDataHandle
*Description: 读字符串
*Input: none
*Output: none
*Return: none
*Others: none
//----------------------------------------------------------------*/
void ConfigModuleDataHandle(char* dat)
{
if(guUartState.Bit.rxStop) // 数据接收完毕
{
if(strlen(gHopeReturnData1) != 0 && strstr(dat, gHopeReturnData1))
{
ConfigModuleRunNext(0);
}
if(strlen(gHopeReturnData2) != 0 && strstr(dat, gHopeReturnData2))
{
ConfigModuleRunNext(0);
}
guUartState.Bit.rxStop = 0; // 信息处理完毕
RxBufferClean();
}
}比对给定的字符串gHopeReturnData1和gHopeReturnData2和返回的数据是否有包含关系,有的话就认为回复结果有效,进行下一步。
/*----------------------------------------------------------------
*Function: 获取联网状态
*Description: GetConnectState
*Input: none
*Output: none
*Return: none
*Others: none
//----------------------------------------------------------------*/
void GetConnectState(char* dat)
{
if(guUartState.Bit.rxStop) // 数据接收完毕
{
if(strlen(gHopeReturnData1) != 0 && strstr(dat, gHopeReturnData1)) // connect ok
{
guModuleInfo.Bit.tcpConnect = 1;
SendConfigFunction(NULL, NULL, NULL, NULL, NULL, MODULE_COMPARE); // 清空函数指针
ConfigModuleRunNext(0);
}
guUartState.Bit.rxStop = 0;
RxBufferClean();
}
}获取模块联网状态,如果联网了就置位相应的flag,这里是guModuleInfo.Bit.tcpConnect,并且清空各个函数指针,为进行TCP通讯做准备
/*----------------------------------------------------------------
*Function: GetSignal
*Description: 获取信号强度
*Input: none
*Output: none
*Return: none
*Others: none
//----------------------------------------------------------------*/
void GetSignal(char* dat)
{
char ptr[3];
//u8 i;
if(guUartState.Bit.rxStop) // 数据接收完毕
{
//ptr = StrBetwString(dat, "\r\n", "\r\n");
FindBetwString(dat, "+CSQ: ", ",", (char*)ptr, sizeof(ptr));
if(strlen((char*)ptr))
{
gsIotProduct.signal = atoi(ptr);
ConfigModuleRunNext(0); // 执行下一条
}
//cStringFree(); // 释放StrBetwString中的控件
guUartState.Bit.rxStop = 0;
RxBufferClean();
}
}
/*----------------------------------------------------------------
*Function: GetSimInfo
*Description: 获取sim ICCID
*Input: none
*Output: none
*Return: none
*Others: none
//----------------------------------------------------------------*/
void GetSimInfo(char* dat)
{
char ptr[sizeof(gsIotProduct.simIccid)];
//u8 i;
if(guUartState.Bit.rxStop) // 数据接收完毕
{
//ptr = StrBetwString(dat, "\r\n", "\r\n");
FindBetwString(dat, "+ICCID: ", "\r\n", (char*)ptr, sizeof(ptr));
if(strlen((char*)ptr) == 20) // IMEI只有15位
{
if(strlen(gsIotProduct.simIccid) == 0)
{
memset(gsIotProduct.simIccid, 0, sizeof(gsIotProduct.simIccid));
memcpy(gsIotProduct.simIccid, ptr, 20);
}
ConfigModuleRunNext(0); // 执行下一条
}
//cStringFree(); // 释放StrBetwString中的控件
guUartState.Bit.rxStop = 0;
RxBufferClean();
}
}
/*----------------------------------------------------------------
*Function: GetDeviceInfo
*Description: 获取设备IMEI
*Input: none
*Output: none
*Return: none
*Others: none
//----------------------------------------------------------------*/
void GetDeviceInfo(char* dat)
{
char ptr[sizeof(gsIotProduct.deviceName)];
if(guUartState.Bit.rxStop) // 数据接收完毕
{
FindBetwString(dat, "\r\n", "\r\n", (char*)ptr, sizeof(ptr));
if(strlen((char*)ptr) == 15) // IMEI只有15位
{
if(strlen(gsIotProduct.deviceName) == 0)
{
memset(gsIotProduct.deviceName, 0, sizeof(gsIotProduct.deviceName));
memcpy(gsIotProduct.deviceName, ptr, 15);
}
ConfigModuleRunNext(0); // 执行下一条
}
//cStringFree(); // 释放StrBetwString中的控件
guUartState.Bit.rxStop = 0;
RxBufferClean();
}
}
/*----------------------------------------------------------------
*Function: GetDeviceIP
*Description: 获取设备IP
*Input: none
*Output: none
*Return: none
*Others: none
//----------------------------------------------------------------*/
void GetDeviceIP(char* dat)
{
char ptr[sizeof(gsIotProduct.deviceName)];
if(guUartState.Bit.rxStop) // 数据接收完毕
{
FindBetwString(dat, "\r\n", "\r\n", (char*)ptr, sizeof(ptr));
if(strlen((char*)ptr) <= sizeof(gsIotProduct.ip))
{
if(strlen(gsIotProduct.ip) == 0)
{
memset(gsIotProduct.ip, 0, sizeof(gsIotProduct.ip));
memcpy(gsIotProduct.ip, ptr, strlen((char*)ptr));
}
guModuleInfo.Bit.netConnect = 1;
ConfigModuleRunNext(0); // 执行下一条
}
//cStringFree(); // 释放StrBetwString中的控件
guUartState.Bit.rxStop = 0;
RxBufferClean();
}
}这几个函数是用FindBetwString函数进行搜寻返回的字符串,获取到相关数据后保存,并进行下一步。
/*----------------------------------------------------------------
*Function: SendConfigFunction
*Description: 发送信息和处理函数
*Input: order: 要发送的字符串
functionSent: 发送数据函数(比如调试用)
hopeReturn1: 接收函数(希望返回的字符串1)
hopeReturn2: 接收函数(希望返回的字符串2)
functionPares: 数据处理函数
nextDelay: 延迟发送下一条指令的时间
*Output: none
*Return: none
*Others: none
//----------------------------------------------------------------*/
void SendConfigFunction(const char* order, void (*functionSent)(void), char* hopeReturn1, char* hopeReturn2, void (*functionPares)(char* dat), u16 nextDelay)
{
memset(gHopeReturnData1, NULL, strlen(gHopeReturnData1)); // 清除数组
memset(gHopeReturnData2, NULL, strlen(gHopeReturnData2)); // 清除数组
ConfigConnectDispose = NULL;
if(hopeReturn1 != NULL) {strcpy(gHopeReturnData1, hopeReturn1);} // 复制字符串到数组
if(hopeReturn2 != NULL) {strcpy(gHopeReturnData2, hopeReturn2);} // 复制字符串到数组
if(functionSent != NULL) {functionSent();} // 发送函数
if(functionPares != NULL) {ConfigConnectDispose = functionPares;} // 处理函数指针赋值
gsCountTime.configModuleCnt = nextDelay; // 等待响应时间
// 这里也可以是自己写的其他发送函数
// 为了和后面mqtt发送统一,这里采用mqtt文件的接口发送
TCPInit(); // 结构体初始化
SetTCPSendDataToServer(UartWriteBuff); // 注册发送函数
SetTCPReceiveDataFromServer(ConfigConnectDispose); // 注册接收函数
SetTCPDelayms(DelayMs);
if(order != NULL)
{
TCPSendDataToBuff((char*)order, strlen(order), TCP_CHAR);
gsTCP.sendDataReady = 1;
}
}这个函数主要的作用就是初始化串口的发送和接收函数,以及各个预期的正确返回字符串的赋值,其中SetTCPSendDataToServer(UartWriteBuff) 是一个接口,将循环发送一个字符串的函数赋值给系统发送指针。SetTCPReceiveDataFromServer(ConfigConnectDispose) 作用一样,也是赋值给一个函数指针用于接收处理返回的字符串数据,这边的接收处理函数就是上面几个。
/*----------------------------------------------------------------
*Function: ConfigModuleNoBlack
*Description: AT指令配置模块
*Input: none
*Output: none
*Return: none
*Others: none
//----------------------------------------------------------------*/
void ConfigModuleNoBlack(void)
{
u8 len = 0;
char ConfigModuleNoBlackBuff[200];
if(gsCountTime.configModuleCnt > 0)
{
gsCountTime.configModuleCnt--;
}
if(gsCountTime.configModuleCnt == 0 && !guModuleInfo.Bit.tcpConnect)
{
//gsCountTime.configModuleCnt = gsCountTime.sendNextDelay;
if(gbDataReturnFlag)
{
gsErrorDeal.inCheck2 = 0;
gbDataReturnFlag = 0;
gConfigModulstep++;
}
else
{
gsErrorDeal.inCheck2++;
if(gsErrorDeal.inCheck2 >= 5) // 超时
{
gsErrorDeal.inCheck2 = 0;
gConfigModulstep = 0; // 重新回到第一步
}
}
switch (gConfigModulstep)
{
case 0: // 重启模块
SendConfigFunction("AT+RESET\r\n", NULL, "OK", NULL, ConfigModuleDataHandle, MODULE_COMPARE);
break;
case 1: // 连接测试
SendConfigFunction("AT\r\n", NULL, "OK", NULL, ConfigModuleDataHandle, MODULE_COMPARE);
break;
case 2: // 关闭回显
SendConfigFunction("ATE0\r\n", NULL, "OK", NULL, ConfigModuleDataHandle, MODULE_COMPARE);
break;
case 3: // 获取IEMI
SendConfigFunction("AT+GSN\r\n", NULL, NULL, NULL, GetDeviceInfo, MODULE_COMPARE);
break;
case 4: // 读取SIM状态
SendConfigFunction("AT+CPIN?\r\n", NULL, "OK", NULL, ConfigModuleDataHandle, MODULE_COMPARE);
break;
case 5: // 获取sim卡号
SendConfigFunction("AT+ICCID\r\n", NULL, NULL, NULL, GetSimInfo, MODULE_COMPARE);
break;
case 6: // 获取信号强度
SendConfigFunction("AT+CSQ\r\n", NULL, NULL, NULL, GetSignal, MODULE_COMPARE);
break;
case 7: // GPRS注册状况
SendConfigFunction("AT+CGREG?\r\n", NULL, "+CGREG: 0,1", NULL, ConfigModuleDataHandle, (MODULE_COMPARE+MODULE_COMPARE));
break;
case 8: // 是否附着GPRS
SendConfigFunction("AT+CGATT?\r\n", NULL, "+CGATT: 1", NULL, ConfigModuleDataHandle, MODULE_COMPARE);
break;
case 9: // 设置保活时间
SendConfigFunction("AT+CIPTKA=1,120,75,9\r\n", NULL, "OK", NULL, ConfigModuleDataHandle, MODULE_COMPARE);
break;
case 10: // 开单路连接
SendConfigFunction("AT+CIPMUX=0\r\n", NULL, "OK", NULL, ConfigModuleDataHandle, MODULE_COMPARE);
break;
case 11: // 设置快发模式
SendConfigFunction("AT+CIPQSEND=1\r\n", NULL, "OK", NULL, ConfigModuleDataHandle, MODULE_COMPARE);
break;
case 12: // 设置APN
SendConfigFunction("AT+CSTT\r\n", NULL, "OK", NULL, ConfigModuleDataHandle, MODULE_COMPARE);
break;
case 13: // 激活移动场景
SendConfigFunction("AT+CIICR\r\n", NULL, "OK", NULL, ConfigModuleDataHandle, MODULE_COMPARE);
break;
case _moduleStepIP: // 查询IP
SendConfigFunction("AT+CIFSR\r\n", NULL, NULL, NULL, GetDeviceIP, MODULE_COMPARE);
ConfigModuleRunNext(TIME_2S);
break;
case _modeleStepTCP: // TCP连接
len = sprintf(ConfigModuleNoBlackBuff, "AT+CIPSTART=\"TCP\",%s,%s\r\n", TCPIp, TCPPort);
ConfigModuleNoBlackBuff[len] = 0;
SendConfigFunction(ConfigModuleNoBlackBuff, NULL, "CONNECT", NULL, ConfigModuleDataHandle, MODULE_COMPARE);
break;
case _moduleStepCheckTCP: // 连接情况
SendConfigFunction("AT+CIPSTATUS\r\n", NULL, "CONNECT OK", NULL, GetConnectState, MODULE_COMPARE); // 等待接收消息
//gMqttStep = 0;
break;
default:
SendConfigFunction(NULL, NULL, NULL, NULL, NULL, MODULE_COMPARE);
gbDataReturnFlag = 0;
break;
}
}
}讲完了上面的几个函数作用之后,这个函数就很容易理解了,从上往下也就是我刚才在TCP AT指令说明这一小结中模拟的初始化流程。每一步给定要发送的指令和正确的返回数据,然后注册好处理函数,设定每一步最多等待的时间。这样就是一个完整的TCP初始化流程了。下面看一下TCP的通讯步骤。
/* TCP结构体 */
typedef struct
{
char* sendBuff; //指向数据发送缓冲区的指针
char* sendBuffPointNow; //指向数据发送缓冲区当前位置的指针
ResultCode_t resultCode; //结果返回代码
BOOL sendDataReady; //发送服务器数据准备OK
void* returnData; //指向服务端返回的对应的报文结构体的指针
void (*TCPSendDataToServer)(const char* data, u16 length);//发送数据-----接口
void (*TCPDelayms)(unsigned short ms); //延时毫秒函数-----接口
void (*TCPReceiveDataFromServer)(char* dat); //从服务器接收到数据-----接口
}TCP_t;
MY_TCP_EXT TCP_t gsTCP;
/* Exported functions ------------------------------------------------------- */
MY_TCP_EXT void TCPInit(void);
MY_TCP_EXT void TCPClean(void);
MY_TCP_EXT void SetTCPSendDataToServer(void (*TCPSendDataToServer)(const char* data, u16 length));
MY_TCP_EXT void SetTCPDelayms(void (*TCPDelayms)(unsigned short ms));
MY_TCP_EXT void SetTCPReceiveDataFromServer(void (*TCPReceiveDataFromServer)(char* data));
MY_TCP_EXT void TCPSendDataToBuff(char* data, unsigned int n, DataType_t dataType);
MY_TCP_EXT void TxBufferClean(void);
MY_TCP_EXT void TCPCommunication(void);
static void TCPRegisterDevice(void);
static void TCPOnLine(void);
static void SendMessageFunction(const char* order, void (*functionSent)(void), char* hopeReturn1, char* hopeReturn2,void (*functionPares)(char* dat), u16 nextDelay);
//static void TCPDataHandle(char* dat);
static void TCPReceiveHandle(char* dat);
static void TCPOnlineRunNext(u16 nextTime);
static void TCPGetProperty(char* dat);
static void TCPPropertyMsg(char* dat, u16 len);
static void TCPHeartBeatHandle(char* dat);
其实步骤也差不多,概况来说就是拼接数据,计算数据宽度,告诉模块需要发送多少字节的数据,然后发送。
/*----------------------------------------------------------------
*Function: TCPInit
*Description: TCP初始化
*Input: none
*Output: none
*Return: none
*Others: none
//----------------------------------------------------------------*/
void TCPInit(void)
{
gsTCP.sendBuff = gTCPBuff; // 发送缓存数组
gsTCP.sendBuffPointNow = gTCPBuff; // 发送缓存数组当前发送的字节位置
gsTCP.resultCode = CODE_DEFAULT; // 服务器发送的代码,MQTT上使用的多,TCP不一定有
gsTCP.returnData = NULL;
SetTCPSendDataToServer(NULL); // 注册发送函数
SetTCPReceiveDataFromServer(NULL); // 注册接收函数
SetTCPDelayms(NULL); // 注册接收函数
TxBufferClean(); // 发送数组清除
}
/*----------------------------------------------------------------
*Function: TCPClean
*Description: TCP清除
*Input: none
*Output: none
*Return: none
*Others: none
//----------------------------------------------------------------*/
void TCPClean(void)
{
gsTCP.sendBuff = NULL;
gsTCP.sendBuffPointNow = NULL;
gsTCP.resultCode = CODE_DEFAULT;
gsTCP.returnData = NULL;
SetTCPSendDataToServer(NULL); // 注册发送函数
SetTCPReceiveDataFromServer(NULL); // 注册接收函数
SetTCPDelayms(NULL);
}TCP初始化函数,主要用于每次和发送接收数据前的指针赋值,每个成员详细功能见上面的注释。
/*----------------------------------------------------------------
*Function: TCPSendDataToBuff
*Description: 将数据拼接到buff
*Input: none
*Output: none
*Return: none
*Others: none
//----------------------------------------------------------------*/
void TCPSendDataToBuff(char* data, unsigned int n, DataType_t dataType)
{
if (data == NULL)
return;
if (dataType == TCP_NUM)
data = (char*)data + n - 1;
while (n--)
{
*gsTCP.sendBuffPointNow++ = *(char*)data;
data = (char*)data + dataType;
}
}数据拼接,每次发送数据时,不同部分的数据拼接用。至于字符串和数字分开处理主要是借用的MQTT部分的代码,而MQTT通讯是直接发送16进制数据,而数字部分储存在不同的系统中分大小端,其中小端的系统需要反着储存。
switch(gTCPOnlineStep)
{
case 0:
TCPPropertyMsg(str, sizeof(str)); // 组合报文
len = strlen(str);
sprintf(str, "AT+CIPSEND=%d\r\n",len);
SendMessageFunction(str, NULL, ">", NULL, TCPReceiveHandle, TIME_1S);
guTCPInfo.Bit.sent = 0; // 开始发送
break;
case 1:
TCPPropertyMsg(str, sizeof(str)); // 组合报文
SendMessageFunction(str, NULL, "DATA ACCEPT", gsIotProduct.deviceName, TCPReceiveHandle, TIME_1S);
break;
case 2:
SendMessageFunction(NULL, NULL, NULL, NULL, TCPGetProperty, TIME_1S); // 等待服务器传回的数据
break;
case 3:
SendMessageFunction(NULL, NULL, HEARTBEAT, gsIotProduct.deviceName, TCPHeartBeatHandle, 0); // 等待服务器传回的数据
guModuleInfo.Bit.slaveUpdata = 0; // 更新结束
guTCPInfo.Bit.sent = 1; // 发送结束
guTCPInfo.Bit.receive = 0; // 接收清零,等待下次接收
default:
break;
}发送的逻辑,和模块初始化部分差不多,AT+CIPSEND=%d\r\n是发送指令,拼接好数据之后计算宽度填充到%d,等待返回">" 然后发送报文,接着就是空闲等待心跳,循环。
TCP直连相对来说是比较简单的一个通讯方式,底层部分都交给了模块,应用层只是发送几个AT指令就可以了。主要注意一下返回不同的字符串进行不同的处理,以及通讯错误时的纠正措施。如果有讲的不明白的可以通过邮件联系我。那么前文提到的数据采集部分有缘再见了:)
本文作者:HelloGakki
本文链接:https://pinaland.cn/archives/development-log-air724ug-tcp-connect.html
版权声明:所有文章除特别声明外均系本人自主创作,本文遵循署名 - 非商业性使用 - 禁止演绎 4.0 国际许可协议,转载请注明出处。
最近工作上遇到一个项目,客户之前使用的是2G模块,用于检测三相电流和各相的温度并通过模块联网上传到服务器。该服务器与模块使用TCP直连,私有协议。目前通知2G网络逐步开始停止维护了,所以需要将2G模块升级成4G模块。之前做过一个使用合宙4G模块和阿里云平台通过MQTT通讯的案子,所以该项目就用这套方案将通讯这部分改一改就能用了。
这次主要是讲一下TCP通讯所需的相关硬件,其他的一些诸如采集数据等电路有空再说吧~




采用中微的BAT32G137,M0核,该芯片内置128K Flash和12K SRAM。其他如12Bit AD转换口,串口,定时器都是常规配置,比较齐全,封装LQFP32,脚位也够用。
合宙Air724ug,支持AT指令。带网络相关服务,支持http,tcp,mqtt。支持文件系统读写,电话本命令,短消息命令,通话和音频相关。总之是一个比较强大的模组。具体可以参考合宙官网

这边主要是注意一下MCU和合宙模块电压匹配问题,MCU使用3.3V供电,所以IO输出也是3.3V。而模块输入电压4.16V,经过内部转换之后输出1.8V。这里就分享一个常用的转换电路。简单说一下原理:电路采用一个三极管,一个二极管,3个电阻组成。左边为模块串口,右边则是MCU的串口。

首先看一下规格书上给出的这份流程图(见下图:TCP流程)。模块上电部分,主要通过MOS和三极管组合来通断电源给模块供电,并且Powerkey脚硬件上直接接地。之后就是一系列的AT指令,在写程序前可以先使用模块带的usb口通过上位机软件先测试一下。

AT+RESET\r\n // 重启模块,一般不从这一步开始
AT\r\n // 测试是否开机
ATE0\r\n // 关闭回显
AT+GSN\r\n // 获取IEMI,用于系统唯一性认证
AT+CPIN?\r\n // 获取SIM状态
AT+ICCID\r\n // 获取SIM卡号,可以上传给服务器
AT+CSQ\r\n // 获取信号强度
AT+CGREG?\r\n // GPRS注册状态
AT+CGATT?\r\n // 是否附着GPRS
AT+CIPMUX=0\r\n // 开单路连接,这个看情况是否需要
AT+CIPQSEND=1\r\n // 设置快发模式
AT+CSTT\r\n // 设置APN
AT+CIICR\r\n // 激活移动场景
AT+CIFSR\r\n // 查询IP,到这里设备初始化完成,后面就需要TCP连接
AT+CIPSTART="TCP",IP,PORT\r\n // 链接服务器 OK CONNECT OK
AT+CIPSTATUS\r\n // 查询下链接状态 STATE: CONNECT OK
AT+CIPSEND=<length> // 发送数据,返回”>“ 表示可以发送具体的数据,数据长度和length相同
AT+CIPCLOSE // 关闭TCP连接,单路直接发送
// 保活,两种方式,前一种是TCP底层,后一种心跳报是应用层,需要加在AT+CIPMUX前
AT+CIPTKA=<mode>[,<keepIdle>[,<keepInterval>[,<keepCount>]]
AT^HEARTCONFIG=<option>,<socket_id>,<heartbeat_time>以上就是使用到的全部AT指令,前面9步是模块基础的初始化部分,10开始是TCP相关指令。详细指令参数说明可以参考Air724UG模块AT指令手册。
着重说一下这些指令中的保活,AT指令有两种保活方式。分为协议层面和应用层面。我分别说一下这两种方式的区别:
AT+CIPTKA=<mode>[,<keepIdle>[,<keepInterval>[,<keepCount>]] // 保活
AT+CIPTKA=1,120,75,9开启保活,120秒时间没有数据交互则每75秒发送一个保活探针,一共9次AT^HEARTCONFIG=<option>,<socket_id>,<heartbeat_time> // 心跳包
AT^HEARTCONFIG=1,0,120开启发送心跳,每120秒发送一个心跳包两种方式中客户端推荐使用方式1,服务端推荐方式2。
[2025-07-24 15:16:18.787] SEND >>>>>>>>>> AT
[2025-07-24 15:16:18.787]
[2025-07-24 15:16:18.804] AT
[2025-07-24 15:16:18.804]
[2025-07-24 15:16:18.804] OK
[2025-07-24 15:16:18.804]
[2025-07-24 15:18:52.743] SEND >>>>>>>>>> ATE0
[2025-07-24 15:18:52.743]
[2025-07-24 15:18:52.766] ATE0
[2025-07-24 15:18:52.766]
[2025-07-24 15:18:52.766] OK
[2025-07-24 15:18:52.766]
[2025-07-24 15:19:12.617] SEND >>>>>>>>>> AT+GSN
[2025-07-24 15:19:12.617]
[2025-07-24 15:19:12.620]
[2025-07-24 15:19:12.620] 861658062303204
[2025-07-24 15:19:12.620]
[2025-07-24 15:19:12.620] OK
[2025-07-24 15:19:12.620]
[2025-07-24 15:19:20.467] SEND >>>>>>>>>> AT+CPIN?
[2025-07-24 15:19:20.467]
[2025-07-24 15:19:20.495]
[2025-07-24 15:19:20.495] +CPIN: READY
[2025-07-24 15:19:20.495]
[2025-07-24 15:19:20.495] OK
[2025-07-24 15:19:20.495]
[2025-07-24 15:19:33.093] SEND >>>>>>>>>> AT+ICCID
[2025-07-24 15:19:33.093]
[2025-07-24 15:19:33.109]
[2025-07-24 15:19:33.109] +ICCID: 89861501112441093935
[2025-07-24 15:19:33.109]
[2025-07-24 15:19:33.109] OK
[2025-07-24 15:19:33.109]
[2025-07-24 15:20:10.767] SEND >>>>>>>>>> AT+CSQ
[2025-07-24 15:20:10.767]
[2025-07-24 15:20:10.783]
[2025-07-24 15:20:10.783] +CSQ: 22,99
[2025-07-24 15:20:10.783]
[2025-07-24 15:20:10.783] OK
[2025-07-24 15:20:10.783]
[2025-07-24 15:20:15.500] SEND >>>>>>>>>> AT+CGREG?
[2025-07-24 15:20:15.500]
[2025-07-24 15:20:15.502]
[2025-07-24 15:20:15.502] +CGREG: 0,1
[2025-07-24 15:20:15.502]
[2025-07-24 15:20:15.502] OK
[2025-07-24 15:20:15.502]
[2025-07-24 15:20:20.159] SEND >>>>>>>>>> AT+CGATT?
[2025-07-24 15:20:20.159]
[2025-07-24 15:20:20.183]
[2025-07-24 15:20:20.183] +CGATT: 1
[2025-07-24 15:20:20.183]
[2025-07-24 15:20:20.183] OK
[2025-07-24 15:20:20.183]
[2025-07-24 15:20:25.925] SEND >>>>>>>>>> AT+CIPMUX=0
[2025-07-24 15:20:25.925]
[2025-07-24 15:20:25.926]
[2025-07-24 15:20:25.926] OK
[2025-07-24 15:20:25.926]
[2025-07-24 15:20:31.700] SEND >>>>>>>>>> AT+CIPTKA=1,120,75,9
[2025-07-24 15:20:31.700]
[2025-07-24 15:20:31.702]
[2025-07-24 15:20:31.702] OK
[2025-07-24 15:20:31.702]
[2025-07-24 15:20:40.109] SEND >>>>>>>>>> AT+CIPQSEND=1
[2025-07-24 15:20:40.109]
[2025-07-24 15:20:40.111]
[2025-07-24 15:20:40.111] OK
[2025-07-24 15:20:40.111]
[2025-07-24 15:20:45.660] SEND >>>>>>>>>> AT+CSTT
[2025-07-24 15:20:45.660]
[2025-07-24 15:20:45.660]
[2025-07-24 15:20:45.660] OK
[2025-07-24 15:20:45.660]
[2025-07-24 15:20:50.124] SEND >>>>>>>>>> AT+CIICR
[2025-07-24 15:20:50.124]
[2025-07-24 15:20:50.127]
[2025-07-24 15:20:50.127] OK
[2025-07-24 15:20:50.127]
[2025-07-24 15:20:54.391] SEND >>>>>>>>>> AT+CIFSR
[2025-07-24 15:20:54.391]
[2025-07-24 15:20:54.393]
[2025-07-24 15:20:54.393] 10.98.97.7
[2025-07-24 15:20:54.393]
[2025-07-24 15:21:02.966] SEND >>>>>>>>>> AT+CIPSTATUS
[2025-07-24 15:21:02.966]
[2025-07-24 15:21:02.980]
[2025-07-24 15:21:02.980] OK
[2025-07-24 15:21:02.980]
[2025-07-24 15:21:02.980] STATE: IP STATUS
[2025-07-24 15:21:02.980]
[2025-07-24 15:21:54.265] SEND >>>>>>>>>> AT+CIPSTART="TCP",112.125.89.8,42898
[2025-07-24 15:21:54.265]
[2025-07-24 15:21:54.271]
[2025-07-24 15:21:54.271] OK
[2025-07-24 15:21:54.271]
[2025-07-24 15:21:54.457]
[2025-07-24 15:21:54.457] CONNECT OK
[2025-07-24 15:21:54.457]
[2025-07-24 15:23:04.223] SEND >>>>>>>>>> AT+CIPSEND=10
[2025-07-24 15:23:04.223]
[2025-07-24 15:23:04.225]
[2025-07-24 15:23:04.225] >
[2025-07-24 15:23:14.639] SEND >>>>>>>>>> HelloWorld
[2025-07-24 15:23:14.639]
[2025-07-24 15:23:14.642]
[2025-07-24 15:23:14.642] DATA ACCEPT:10 以上是使用上位机串口工具和模块通讯初始化的过程,其中【SEND >>>>>>>>>>】是上位机发送的数据,其他为模块回传数据,空行表示“\r\n”,也就是换行符。有了上述过程,那么程序部分就很容易了。另外提一嘴,4G模块和WIFI模块不一样,不能使用局域网调试TCP,需要配置公网环境,这个比较麻烦。这里推荐一个网站可以开启公网TCP端口用于调试,TCP在线调试网站。
先来看看设备初始化相关部分代码
WORK_EXT void ConfigModuleNoBlack(void);
WORK_EXT void (*ConfigConnectDispose)(char* dat); // 处理函数的指针,放在while循环中
WORK_EXT void SendConfigFunction(const char* order, void (*functionSent)(void), char* hopeReturn1, char* hopeReturn2, void (*functionPares)(char* dat), u16 nextDelay);
WORK_EXT void GetDeviceIP(char* dat);
WORK_EXT void GetDeviceInfo(char* dat);
WORK_EXT void GetSimInfo(char* dat);
WORK_EXT void GetSignal(char* dat);
WORK_EXT void GetConnectState(char* dat);
WORK_EXT void ConfigModuleDataHandle(char* dat);
WORK_EXT void ConfigModuleRunNext(u16 nextTime);从下往上看,是处理不同回复的函数,最后在ConfigModuleNoBlack函数中进行各个功能使用。
/*----------------------------------------------------------------
*Function: ConfigModuleRunNext
*Description: 设置下一条指令运行时间
*Input: none
*Output: none
*Return: none
*Others: none
//----------------------------------------------------------------*/
void ConfigModuleRunNext(u16 nextTime)
{
gbDataReturnFlag = 1;
//gsCountTime.sendNextDelay = nextTime;
gsCountTime.configModuleCnt = nextTime;
}搭配下面几个函数中使用,表示收到有效的回复,进行下一步
/*----------------------------------------------------------------
*Function: ConfigModuleDataHandle
*Description: 读字符串
*Input: none
*Output: none
*Return: none
*Others: none
//----------------------------------------------------------------*/
void ConfigModuleDataHandle(char* dat)
{
if(guUartState.Bit.rxStop) // 数据接收完毕
{
if(strlen(gHopeReturnData1) != 0 && strstr(dat, gHopeReturnData1))
{
ConfigModuleRunNext(0);
}
if(strlen(gHopeReturnData2) != 0 && strstr(dat, gHopeReturnData2))
{
ConfigModuleRunNext(0);
}
guUartState.Bit.rxStop = 0; // 信息处理完毕
RxBufferClean();
}
}比对给定的字符串gHopeReturnData1和gHopeReturnData2和返回的数据是否有包含关系,有的话就认为回复结果有效,进行下一步。
/*----------------------------------------------------------------
*Function: 获取联网状态
*Description: GetConnectState
*Input: none
*Output: none
*Return: none
*Others: none
//----------------------------------------------------------------*/
void GetConnectState(char* dat)
{
if(guUartState.Bit.rxStop) // 数据接收完毕
{
if(strlen(gHopeReturnData1) != 0 && strstr(dat, gHopeReturnData1)) // connect ok
{
guModuleInfo.Bit.tcpConnect = 1;
SendConfigFunction(NULL, NULL, NULL, NULL, NULL, MODULE_COMPARE); // 清空函数指针
ConfigModuleRunNext(0);
}
guUartState.Bit.rxStop = 0;
RxBufferClean();
}
}获取模块联网状态,如果联网了就置位相应的flag,这里是guModuleInfo.Bit.tcpConnect,并且清空各个函数指针,为进行TCP通讯做准备
/*----------------------------------------------------------------
*Function: GetSignal
*Description: 获取信号强度
*Input: none
*Output: none
*Return: none
*Others: none
//----------------------------------------------------------------*/
void GetSignal(char* dat)
{
char ptr[3];
//u8 i;
if(guUartState.Bit.rxStop) // 数据接收完毕
{
//ptr = StrBetwString(dat, "\r\n", "\r\n");
FindBetwString(dat, "+CSQ: ", ",", (char*)ptr, sizeof(ptr));
if(strlen((char*)ptr))
{
gsIotProduct.signal = atoi(ptr);
ConfigModuleRunNext(0); // 执行下一条
}
//cStringFree(); // 释放StrBetwString中的控件
guUartState.Bit.rxStop = 0;
RxBufferClean();
}
}
/*----------------------------------------------------------------
*Function: GetSimInfo
*Description: 获取sim ICCID
*Input: none
*Output: none
*Return: none
*Others: none
//----------------------------------------------------------------*/
void GetSimInfo(char* dat)
{
char ptr[sizeof(gsIotProduct.simIccid)];
//u8 i;
if(guUartState.Bit.rxStop) // 数据接收完毕
{
//ptr = StrBetwString(dat, "\r\n", "\r\n");
FindBetwString(dat, "+ICCID: ", "\r\n", (char*)ptr, sizeof(ptr));
if(strlen((char*)ptr) == 20) // IMEI只有15位
{
if(strlen(gsIotProduct.simIccid) == 0)
{
memset(gsIotProduct.simIccid, 0, sizeof(gsIotProduct.simIccid));
memcpy(gsIotProduct.simIccid, ptr, 20);
}
ConfigModuleRunNext(0); // 执行下一条
}
//cStringFree(); // 释放StrBetwString中的控件
guUartState.Bit.rxStop = 0;
RxBufferClean();
}
}
/*----------------------------------------------------------------
*Function: GetDeviceInfo
*Description: 获取设备IMEI
*Input: none
*Output: none
*Return: none
*Others: none
//----------------------------------------------------------------*/
void GetDeviceInfo(char* dat)
{
char ptr[sizeof(gsIotProduct.deviceName)];
if(guUartState.Bit.rxStop) // 数据接收完毕
{
FindBetwString(dat, "\r\n", "\r\n", (char*)ptr, sizeof(ptr));
if(strlen((char*)ptr) == 15) // IMEI只有15位
{
if(strlen(gsIotProduct.deviceName) == 0)
{
memset(gsIotProduct.deviceName, 0, sizeof(gsIotProduct.deviceName));
memcpy(gsIotProduct.deviceName, ptr, 15);
}
ConfigModuleRunNext(0); // 执行下一条
}
//cStringFree(); // 释放StrBetwString中的控件
guUartState.Bit.rxStop = 0;
RxBufferClean();
}
}
/*----------------------------------------------------------------
*Function: GetDeviceIP
*Description: 获取设备IP
*Input: none
*Output: none
*Return: none
*Others: none
//----------------------------------------------------------------*/
void GetDeviceIP(char* dat)
{
char ptr[sizeof(gsIotProduct.deviceName)];
if(guUartState.Bit.rxStop) // 数据接收完毕
{
FindBetwString(dat, "\r\n", "\r\n", (char*)ptr, sizeof(ptr));
if(strlen((char*)ptr) <= sizeof(gsIotProduct.ip))
{
if(strlen(gsIotProduct.ip) == 0)
{
memset(gsIotProduct.ip, 0, sizeof(gsIotProduct.ip));
memcpy(gsIotProduct.ip, ptr, strlen((char*)ptr));
}
guModuleInfo.Bit.netConnect = 1;
ConfigModuleRunNext(0); // 执行下一条
}
//cStringFree(); // 释放StrBetwString中的控件
guUartState.Bit.rxStop = 0;
RxBufferClean();
}
}这几个函数是用FindBetwString函数进行搜寻返回的字符串,获取到相关数据后保存,并进行下一步。
/*----------------------------------------------------------------
*Function: SendConfigFunction
*Description: 发送信息和处理函数
*Input: order: 要发送的字符串
functionSent: 发送数据函数(比如调试用)
hopeReturn1: 接收函数(希望返回的字符串1)
hopeReturn2: 接收函数(希望返回的字符串2)
functionPares: 数据处理函数
nextDelay: 延迟发送下一条指令的时间
*Output: none
*Return: none
*Others: none
//----------------------------------------------------------------*/
void SendConfigFunction(const char* order, void (*functionSent)(void), char* hopeReturn1, char* hopeReturn2, void (*functionPares)(char* dat), u16 nextDelay)
{
memset(gHopeReturnData1, NULL, strlen(gHopeReturnData1)); // 清除数组
memset(gHopeReturnData2, NULL, strlen(gHopeReturnData2)); // 清除数组
ConfigConnectDispose = NULL;
if(hopeReturn1 != NULL) {strcpy(gHopeReturnData1, hopeReturn1);} // 复制字符串到数组
if(hopeReturn2 != NULL) {strcpy(gHopeReturnData2, hopeReturn2);} // 复制字符串到数组
if(functionSent != NULL) {functionSent();} // 发送函数
if(functionPares != NULL) {ConfigConnectDispose = functionPares;} // 处理函数指针赋值
gsCountTime.configModuleCnt = nextDelay; // 等待响应时间
// 这里也可以是自己写的其他发送函数
// 为了和后面mqtt发送统一,这里采用mqtt文件的接口发送
TCPInit(); // 结构体初始化
SetTCPSendDataToServer(UartWriteBuff); // 注册发送函数
SetTCPReceiveDataFromServer(ConfigConnectDispose); // 注册接收函数
SetTCPDelayms(DelayMs);
if(order != NULL)
{
TCPSendDataToBuff((char*)order, strlen(order), TCP_CHAR);
gsTCP.sendDataReady = 1;
}
}这个函数主要的作用就是初始化串口的发送和接收函数,以及各个预期的正确返回字符串的赋值,其中SetTCPSendDataToServer(UartWriteBuff) 是一个接口,将循环发送一个字符串的函数赋值给系统发送指针。SetTCPReceiveDataFromServer(ConfigConnectDispose) 作用一样,也是赋值给一个函数指针用于接收处理返回的字符串数据,这边的接收处理函数就是上面几个。
/*----------------------------------------------------------------
*Function: ConfigModuleNoBlack
*Description: AT指令配置模块
*Input: none
*Output: none
*Return: none
*Others: none
//----------------------------------------------------------------*/
void ConfigModuleNoBlack(void)
{
u8 len = 0;
char ConfigModuleNoBlackBuff[200];
if(gsCountTime.configModuleCnt > 0)
{
gsCountTime.configModuleCnt--;
}
if(gsCountTime.configModuleCnt == 0 && !guModuleInfo.Bit.tcpConnect)
{
//gsCountTime.configModuleCnt = gsCountTime.sendNextDelay;
if(gbDataReturnFlag)
{
gsErrorDeal.inCheck2 = 0;
gbDataReturnFlag = 0;
gConfigModulstep++;
}
else
{
gsErrorDeal.inCheck2++;
if(gsErrorDeal.inCheck2 >= 5) // 超时
{
gsErrorDeal.inCheck2 = 0;
gConfigModulstep = 0; // 重新回到第一步
}
}
switch (gConfigModulstep)
{
case 0: // 重启模块
SendConfigFunction("AT+RESET\r\n", NULL, "OK", NULL, ConfigModuleDataHandle, MODULE_COMPARE);
break;
case 1: // 连接测试
SendConfigFunction("AT\r\n", NULL, "OK", NULL, ConfigModuleDataHandle, MODULE_COMPARE);
break;
case 2: // 关闭回显
SendConfigFunction("ATE0\r\n", NULL, "OK", NULL, ConfigModuleDataHandle, MODULE_COMPARE);
break;
case 3: // 获取IEMI
SendConfigFunction("AT+GSN\r\n", NULL, NULL, NULL, GetDeviceInfo, MODULE_COMPARE);
break;
case 4: // 读取SIM状态
SendConfigFunction("AT+CPIN?\r\n", NULL, "OK", NULL, ConfigModuleDataHandle, MODULE_COMPARE);
break;
case 5: // 获取sim卡号
SendConfigFunction("AT+ICCID\r\n", NULL, NULL, NULL, GetSimInfo, MODULE_COMPARE);
break;
case 6: // 获取信号强度
SendConfigFunction("AT+CSQ\r\n", NULL, NULL, NULL, GetSignal, MODULE_COMPARE);
break;
case 7: // GPRS注册状况
SendConfigFunction("AT+CGREG?\r\n", NULL, "+CGREG: 0,1", NULL, ConfigModuleDataHandle, (MODULE_COMPARE+MODULE_COMPARE));
break;
case 8: // 是否附着GPRS
SendConfigFunction("AT+CGATT?\r\n", NULL, "+CGATT: 1", NULL, ConfigModuleDataHandle, MODULE_COMPARE);
break;
case 9: // 设置保活时间
SendConfigFunction("AT+CIPTKA=1,120,75,9\r\n", NULL, "OK", NULL, ConfigModuleDataHandle, MODULE_COMPARE);
break;
case 10: // 开单路连接
SendConfigFunction("AT+CIPMUX=0\r\n", NULL, "OK", NULL, ConfigModuleDataHandle, MODULE_COMPARE);
break;
case 11: // 设置快发模式
SendConfigFunction("AT+CIPQSEND=1\r\n", NULL, "OK", NULL, ConfigModuleDataHandle, MODULE_COMPARE);
break;
case 12: // 设置APN
SendConfigFunction("AT+CSTT\r\n", NULL, "OK", NULL, ConfigModuleDataHandle, MODULE_COMPARE);
break;
case 13: // 激活移动场景
SendConfigFunction("AT+CIICR\r\n", NULL, "OK", NULL, ConfigModuleDataHandle, MODULE_COMPARE);
break;
case _moduleStepIP: // 查询IP
SendConfigFunction("AT+CIFSR\r\n", NULL, NULL, NULL, GetDeviceIP, MODULE_COMPARE);
ConfigModuleRunNext(TIME_2S);
break;
case _modeleStepTCP: // TCP连接
len = sprintf(ConfigModuleNoBlackBuff, "AT+CIPSTART=\"TCP\",%s,%s\r\n", TCPIp, TCPPort);
ConfigModuleNoBlackBuff[len] = 0;
SendConfigFunction(ConfigModuleNoBlackBuff, NULL, "CONNECT", NULL, ConfigModuleDataHandle, MODULE_COMPARE);
break;
case _moduleStepCheckTCP: // 连接情况
SendConfigFunction("AT+CIPSTATUS\r\n", NULL, "CONNECT OK", NULL, GetConnectState, MODULE_COMPARE); // 等待接收消息
//gMqttStep = 0;
break;
default:
SendConfigFunction(NULL, NULL, NULL, NULL, NULL, MODULE_COMPARE);
gbDataReturnFlag = 0;
break;
}
}
}讲完了上面的几个函数作用之后,这个函数就很容易理解了,从上往下也就是我刚才在TCP AT指令说明这一小结中模拟的初始化流程。每一步给定要发送的指令和正确的返回数据,然后注册好处理函数,设定每一步最多等待的时间。这样就是一个完整的TCP初始化流程了。下面看一下TCP的通讯步骤。
/* TCP结构体 */
typedef struct
{
char* sendBuff; //指向数据发送缓冲区的指针
char* sendBuffPointNow; //指向数据发送缓冲区当前位置的指针
ResultCode_t resultCode; //结果返回代码
BOOL sendDataReady; //发送服务器数据准备OK
void* returnData; //指向服务端返回的对应的报文结构体的指针
void (*TCPSendDataToServer)(const char* data, u16 length);//发送数据-----接口
void (*TCPDelayms)(unsigned short ms); //延时毫秒函数-----接口
void (*TCPReceiveDataFromServer)(char* dat); //从服务器接收到数据-----接口
}TCP_t;
MY_TCP_EXT TCP_t gsTCP;
/* Exported functions ------------------------------------------------------- */
MY_TCP_EXT void TCPInit(void);
MY_TCP_EXT void TCPClean(void);
MY_TCP_EXT void SetTCPSendDataToServer(void (*TCPSendDataToServer)(const char* data, u16 length));
MY_TCP_EXT void SetTCPDelayms(void (*TCPDelayms)(unsigned short ms));
MY_TCP_EXT void SetTCPReceiveDataFromServer(void (*TCPReceiveDataFromServer)(char* data));
MY_TCP_EXT void TCPSendDataToBuff(char* data, unsigned int n, DataType_t dataType);
MY_TCP_EXT void TxBufferClean(void);
MY_TCP_EXT void TCPCommunication(void);
static void TCPRegisterDevice(void);
static void TCPOnLine(void);
static void SendMessageFunction(const char* order, void (*functionSent)(void), char* hopeReturn1, char* hopeReturn2,void (*functionPares)(char* dat), u16 nextDelay);
//static void TCPDataHandle(char* dat);
static void TCPReceiveHandle(char* dat);
static void TCPOnlineRunNext(u16 nextTime);
static void TCPGetProperty(char* dat);
static void TCPPropertyMsg(char* dat, u16 len);
static void TCPHeartBeatHandle(char* dat);
其实步骤也差不多,概况来说就是拼接数据,计算数据宽度,告诉模块需要发送多少字节的数据,然后发送。
/*----------------------------------------------------------------
*Function: TCPInit
*Description: TCP初始化
*Input: none
*Output: none
*Return: none
*Others: none
//----------------------------------------------------------------*/
void TCPInit(void)
{
gsTCP.sendBuff = gTCPBuff; // 发送缓存数组
gsTCP.sendBuffPointNow = gTCPBuff; // 发送缓存数组当前发送的字节位置
gsTCP.resultCode = CODE_DEFAULT; // 服务器发送的代码,MQTT上使用的多,TCP不一定有
gsTCP.returnData = NULL;
SetTCPSendDataToServer(NULL); // 注册发送函数
SetTCPReceiveDataFromServer(NULL); // 注册接收函数
SetTCPDelayms(NULL); // 注册接收函数
TxBufferClean(); // 发送数组清除
}
/*----------------------------------------------------------------
*Function: TCPClean
*Description: TCP清除
*Input: none
*Output: none
*Return: none
*Others: none
//----------------------------------------------------------------*/
void TCPClean(void)
{
gsTCP.sendBuff = NULL;
gsTCP.sendBuffPointNow = NULL;
gsTCP.resultCode = CODE_DEFAULT;
gsTCP.returnData = NULL;
SetTCPSendDataToServer(NULL); // 注册发送函数
SetTCPReceiveDataFromServer(NULL); // 注册接收函数
SetTCPDelayms(NULL);
}TCP初始化函数,主要用于每次和发送接收数据前的指针赋值,每个成员详细功能见上面的注释。
/*----------------------------------------------------------------
*Function: TCPSendDataToBuff
*Description: 将数据拼接到buff
*Input: none
*Output: none
*Return: none
*Others: none
//----------------------------------------------------------------*/
void TCPSendDataToBuff(char* data, unsigned int n, DataType_t dataType)
{
if (data == NULL)
return;
if (dataType == TCP_NUM)
data = (char*)data + n - 1;
while (n--)
{
*gsTCP.sendBuffPointNow++ = *(char*)data;
data = (char*)data + dataType;
}
}数据拼接,每次发送数据时,不同部分的数据拼接用。至于字符串和数字分开处理主要是借用的MQTT部分的代码,而MQTT通讯是直接发送16进制数据,而数字部分储存在不同的系统中分大小端,其中小端的系统需要反着储存。
switch(gTCPOnlineStep)
{
case 0:
TCPPropertyMsg(str, sizeof(str)); // 组合报文
len = strlen(str);
sprintf(str, "AT+CIPSEND=%d\r\n",len);
SendMessageFunction(str, NULL, ">", NULL, TCPReceiveHandle, TIME_1S);
guTCPInfo.Bit.sent = 0; // 开始发送
break;
case 1:
TCPPropertyMsg(str, sizeof(str)); // 组合报文
SendMessageFunction(str, NULL, "DATA ACCEPT", gsIotProduct.deviceName, TCPReceiveHandle, TIME_1S);
break;
case 2:
SendMessageFunction(NULL, NULL, NULL, NULL, TCPGetProperty, TIME_1S); // 等待服务器传回的数据
break;
case 3:
SendMessageFunction(NULL, NULL, HEARTBEAT, gsIotProduct.deviceName, TCPHeartBeatHandle, 0); // 等待服务器传回的数据
guModuleInfo.Bit.slaveUpdata = 0; // 更新结束
guTCPInfo.Bit.sent = 1; // 发送结束
guTCPInfo.Bit.receive = 0; // 接收清零,等待下次接收
default:
break;
}发送的逻辑,和模块初始化部分差不多,AT+CIPSEND=%d\r\n是发送指令,拼接好数据之后计算宽度填充到%d,等待返回">" 然后发送报文,接着就是空闲等待心跳,循环。
TCP直连相对来说是比较简单的一个通讯方式,底层部分都交给了模块,应用层只是发送几个AT指令就可以了。主要注意一下返回不同的字符串进行不同的处理,以及通讯错误时的纠正措施。如果有讲的不明白的可以通过邮件联系我。那么前文提到的数据采集部分有缘再见了:)
本文作者:HelloGakki
本文链接:https://pinaland.cn/archives/development-log-air724ug-tcp-connect.html
版权声明:所有文章除特别声明外均系本人自主创作,本文遵循署名 - 非商业性使用 - 禁止演绎 4.0 国际许可协议,转载请注明出处。