前言

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

硬件部分

这次主要是讲一下TCP通讯所需的相关硬件,其他的一些诸如采集数据等电路有空再说吧~

电源

  1. 交流转直流部分这次图省事采用金升阳的ACDC模块,一般这种项目的溢价值相对较高,所以可以稍微奢侈一点;
  2. 直流部分先经过LDO整流,合宙的这个模块VBAT=3.3V~4.3V,参考合宙的demo选用MIC29302降压到4.16V;
  3. MCU供电电压典型值3.3V,考虑到MIC29302的电流够用,就将该LDO输出电压通过一个二极管降压到3.46V使用;
模块电气参数
模块电气参数
ADDC模块
ADDC模块
LDO
LDO
MCU电源
MCU电源

主控芯片

采用中微的BAT32G137,M0核,该芯片内置128K Flash和12K SRAM。其他如12Bit AD转换口,串口,定时器都是常规配置,比较齐全,封装LQFP32,脚位也够用。

4G模块

合宙Air724ug,支持AT指令。带网络相关服务,支持http,tcp,mqtt。支持文件系统读写,电话本命令,短消息命令,通话和音频相关。总之是一个比较强大的模组。具体可以参考合宙官网

合宙模块
合宙模块

Uart通讯部分

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

  1. 当MCU发送数据,MCU_TXD高电平时:D1左边电压1.8V,右边电压3.3V,二极管截止,所以MODULE_RXD为高电平1.8V;
  2. 当MCU发送数据,MCU_TXD低电平时:D1左边电压1.8V,右边电压0V,二极管导通,所以MODULE_RXD为低电平0V;
  3. 当模块发送数据,MODULE_TXD高电平时:三极管UBE=0V,Ib=0,三极管处于截止状态,所以MCU_RXD等于上拉电阻的电压3.3V;
  4. 当模块发送数据,MODULE_TXD低电平时:三极管UBE=1.8V,三极管处于饱和状态导通,所以MCU_RXD等于MODULE_TXD电压0V;
uart电平转换
uart电平转换

软件部分

TCP初始化流程

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

TCP流程
TCP流程

TCP AT指令说明

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指令有两种保活方式。分为协议层面和应用层面。我分别说一下这两种方式的区别:

  1. AT+CIPTKA=<mode>[,<keepIdle>[,<keepInterval>[,<keepCount>]] // 保活

    • mode:是否开启保活,1开启,0关闭;
    • keepIdle:keepIdle时间内没有数据交互则发送保活探针;
    • keepInterval:保活探针的间隔时间
    • keepCount:发送保活探针的数量
    • e.g.:AT+CIPTKA=1,120,75,9开启保活,120秒时间没有数据交互则每75秒发送一个保活探针,一共9次
    • 这个方式发送方和接收方都看不到实际数据,不会影响报文
  2. AT^HEARTCONFIG=<option>,<socket_id>,<heartbeat_time> // 心跳包

    • option:是否开启发送心跳包,1开启,0关闭;
    • socket_id:连接id,定义同+CIPSTART中,这里我们时单路连接,直接填0
    • heartbeat_time:心跳间隔时间
    • e.g.:AT^HEARTCONFIG=1,0,120开启发送心跳,每120秒发送一个心跳包
    • 这个方式是应用层发送报文数据,也就是对方能看到你发送的实际内容,默认是IMEI

两种方式中客户端推荐使用方式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();
    }
}

比对给定的字符串gHopeReturnData1gHopeReturnData2和返回的数据是否有包含关系,有的话就认为回复结果有效,进行下一步。

/*----------------------------------------------------------------
  *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 国际许可协议,转载请注明出处。