通讯框架讲解

这一章节重点讲解通讯框架的实现原理,理论的东西比较多,第一遍可以粗略的过一下,大概清楚这通讯模型,配套案例动手做一下,再回过头来巩固一下原理,多玩几遍,熟悉过后,自己想怎么自定义协议都可以。

代码框架

软件APP部分分为两层

  • uart协议解析和封装的串口HAL层
    • UartContext:串口的实体控制层,提供串口的开关,发送,接收接口
    • ProtocolData:定义通讯的数据结构体,用于保存通讯协议转化出来的实际变量;
    • ProtocolSender:完成数据发送的封装;
    • ProtocolParser:完成数据的协议解析部分,然后将解析好的数据放到ProtocolData的数据结构中;同时管理了应用监听串口数据变化的回调接口;
  • APP应用接口层
    • 通过ProtocolParser提供的接口注册串口数据接收监听获取串口更新出来的ProtocolData。
    • 通过ProtocolSender提供的接口往MCU发送指令信息

我们再细化一下这流程:

可以清楚的看到 接收发送 两路流程一上一下,每一层的功能还是比较清晰的;

具体到代码对应的流程:

无论是接收还是发送流程,最终都是要经过 UartContext 对串口进行读写操作,这是一些标准化的流程,所以 UartContext 我们基本上是不用去修改的,也可以不用理会它是怎么实现的,当然,有兴趣的可以去看一下。

到此,我们对这个通讯模型有个大概的了解,之后我们再来看具体代码的实现。

协议接收部分使用和修改方法

通讯协议格式修改

这里我们举一个比较常见的通讯协议例子:

协议头(2字节) 命令(2字节) 数据长度(1字节) 数据(N) 校验(1字节 可选)
0xFF55 Cmd len data checksum

CommDef.h 文件中定义了同步帧头信息及最小数据包大小信息:

// 需要打印协议数据时,打开以下宏
//#define DEBUG_PRO_DATA

// 支持checksum校验,打开以下宏
//#define PRO_SUPPORT_CHECK_SUM

/* SynchFrame CmdID  DataLen Data CheckSum (可选) */
/*     2Byte  2Byte   1Byte    N Byte  1Byte */
// 有CheckSum情况下最小长度: 2 + 2 + 1 + 1 = 6
// 无CheckSum情况下最小长度: 2 + 2 + 1 = 5

#ifdef PRO_SUPPORT_CHECK_SUM
#define DATA_PACKAGE_MIN_LEN        6
#else
#define DATA_PACKAGE_MIN_LEN        5
#endif

// 同步帧头
#define CMD_HEAD1    0xFF
#define CMD_HEAD2    0x55

ProtocolParser.cpp 文件,配置文件命令格式:

/**
 * 功能:解析协议
 * 参数:pData 协议数据,len 数据长度
 * 返回值:实际解析协议的长度
 */
int parseProtocol(const BYTE *pData, UINT len) {
    UINT remainLen = len;    // 剩余数据长度
    UINT dataLen;    // 数据包长度
    UINT frameLen;    // 帧长度

    /**
     * 以下部分需要根据协议格式进行相应的修改,解析出每一帧的数据
     */
    while (remainLen >= DATA_PACKAGE_MIN_LEN) {
        // 找到一帧数据的数据头
        while ((remainLen >= 2) && ((pData[0] != CMD_HEAD1) || (pData[1] != CMD_HEAD2))) {
            pData++;
            remainLen--;
            continue;
        }

        if (remainLen < DATA_PACKAGE_MIN_LEN) {
            break;
        }

        dataLen = pData[4];
        frameLen = dataLen + DATA_PACKAGE_MIN_LEN;
        if (frameLen > remainLen) {
            // 数据内容不全
            break;
        }

        // 打印一帧数据,需要时在CommDef.h文件中打开DEBUG_PRO_DATA宏
#ifdef DEBUG_PRO_DATA
        for (int i = 0; i < frameLen; ++i) {
            LOGD("%x ", pData[i]);
        }
        LOGD("\n");
#endif

        // 支持checksum校验,需要时在CommDef.h文件中打开PRO_SUPPORT_CHECK_SUM宏
#ifdef PRO_SUPPORT_CHECK_SUM
        // 检测校验码
        if (getCheckSum(pData, frameLen - 1) == pData[frameLen - 1]) {
            // 解析一帧数据
            procParse(pData, frameLen);
        } else {
            LOGE("CheckSum error!!!!!!\n");
        }
#else
        // 解析一帧数据
        procParse(pData, frameLen);
#endif

        pData += frameLen;
        remainLen -= frameLen;
    }

    return len - remainLen;
}

上面的解析流程有点复杂,下面我们先给出一张图,再来分析可能会容易理解一些;一包数据可能包含0到多帧数据,下面这张图里,我们标出来有3帧数据,另外还有一帧数据不全,还少5个数据,不完整的那一帧数据将会拼接到下一包数据里

  • 协议头需要修改
// 1.修改协议头部分的定义,如果协议头长度有变化,则要注意修改协议头判断部分语句。
#define CMD_HEAD1    0xFF
#define CMD_HEAD2    0x55

// 2.协议头长度变化的时候需要修改这里。
while ((mDataBufLen >= 2) && ((pData[0] != CMD_HEAD1) || (pData[1] != CMD_HEAD2)))
  • 协议长度的位置或者长度计算方式发生变化的修改
// 这里的pData[4] 代表的是第5个数据是长度的字节,如果变化了在这里修改一下。
dataLen = pData[4];
// 帧长度一般是数据长度加上头尾长度。如果协议中传的长度计算方式发生变化修改这个部分。
frameLen = dataLen + DATA_PACKAGE_MIN_LEN;
  • 校验发生变化的情况
/**
 * 默认我们是关闭checksum校验的,如果需要支持checksum校验,在CommDef.h文件中打开PRO_SUPPORT_CHECK_SUM宏
 * 当校验不一样的时候需要修改校验方法,
 * 1.校验内容变化修改这个位置
 *     if (getCheckSum(pData, frameLen - 1) == pData[frameLen - 1])
 * 2.校验计算公式变化修改 getCheckSum函数里边的内容
 */

/**
 * 获取校验码
 */
BYTE getCheckSum(const BYTE *pData, int len) {
    int sum = 0;
    for (int i = 0; i < len; ++i) {
        sum += pData[i];
    }

    return (BYTE) (~sum + 1);
}
  • 当完成一帧数据的接收后程序会调用procParse 解析
    // 支持checksum校验,需要时在CommDef.h文件中打开PRO_SUPPORT_CHECK_SUM宏
#ifdef PRO_SUPPORT_CHECK_SUM
    // 检测校验码
    if (getCheckSum(pData, frameLen - 1) == pData[frameLen - 1]) {
        // 解析一帧数据
        procParse(pData, frameLen);
    } else {
        LOGE("CheckSum error!!!!!!\n");
    }
#else
    // 解析一帧数据
    procParse(pData, frameLen);
#endif

通讯协议数据怎么和UI控件对接

继续前面的协议框架我们进入到procParse的解析部分。 这里重点的代码是:ProtocolParser.cpp 打开文件然后找到void procParse(const BYTE *pData, UINT len)

/*
 * 协议解析
 * 输入参数:
 *     pData: 一帧数据的起始地址
 *     len: 帧数据的长度
 */
void procParse(const BYTE *pData, UINT len) {
    /*
     * 解析Cmd值获取数据赋值到sProtocolData结构体中
     */
    switch (MAKEWORD(pData[2], pData[3])) {
    case CMDID_POWER:
        sProtocolData.power = pData[5];
        LOGD("power status:%d",sProtocolData.power);
        break;
    }
    notifyProtocolDataUpdate(sProtocolData);
}

以上 MAKEWORD(pData[2], pData[3]) 在我们的协议例子中表示Cmd值; 当数据解析完成后通过notifyProtocolDataUpdate 通知到页面UI更新,这个部分请参照后面的UI更新部分

  • 数据结构

上面的协议解析到了sProtocolData 结构体中,sProtocolData 是一个静态的变量,用于保存MCU(或者其他设备)串口发送过来的数据值。 这个数据结构在ProtocolData.h文件中。这里可以添加整个项目里面需要使用到的通讯变量

typedef struct {
    // 可以在这里面添加协议的数据变量
    BYTE power;
} SProtocolData;
  • UI更新

UI界面在工具生成Activity.cpp的时候就已经完成了registerProtocolDataUpdateListener ,也就是说当数据更新的时候logic里面页面程序就会收到数据。

static void onProtocolDataUpdate(const SProtocolData &data) {
    // 串口数据回调接口
    if (mProtocolData.power != data.power) {
        mProtocolData.power = data.power;
    }

    if (mProtocolData.eRunMode != data.eRunMode) {
        mProtocolData.eRunMode = data.eRunMode;
        mbtn_autoPtr->setSelected(mProtocolData.eRunMode == E_RUN_MODE_MANUAL);
        if (mProtocolData.eRunMode != E_RUN_MODE_MANUAL) {
            mbtn_external_windPtr->setText(mProtocolData.externalWindSpeedLevel);
            mbtn_internal_windPtr->setText(mProtocolData.internalWindSpeedLevel);
        }
    }
    ...
}

在代码里面我们看到一个变量 mProtocolData,这是一个页面里面的static 的变量。在onUI_init()的时候会初始化。 如:

static SProtocolData mProtocolData;
static void onUI_init() {
    //Tips :添加 UI初始化的显示代码到这里,如:mText1->setText("123");
    mProtocolData = getProtocolData(); // 初始化串口数据的结构体。
    // 开始初始化页面的UI显示
}

串口数据发送

打开ProtocolSender.cpp 当APP层需要发送数据给MCU(或其他设备)的时候直接调用sendProtocol 就可以了。 具体的协议封装由sendProtocol方法完成。用户可以根据自己的协议要求修改这个部分的代码。

/**
 * 需要根据协议格式进行拼接,以下只是个模板
 */
bool sendProtocol(const UINT16 cmdID, const BYTE *pData, BYTE len) {
    BYTE dataBuf[256];

    dataBuf[0] = CMD_HEAD1;
    dataBuf[1] = CMD_HEAD2;            // 同步帧头

    dataBuf[2] = HIBYTE(cmdID);
    dataBuf[3] = LOBYTE(cmdID);        // 命令字节

    dataBuf[4] = len;

    UINT frameLen = 5;

    // 数据
    for (int i = 0; i < len; ++i) {
        dataBuf[frameLen] = pData[i];
        frameLen++;
    }

#ifdef PRO_SUPPORT_CHECK_SUM
    // 校验码
    dataBuf[frameLen] = getCheckSum(dataBuf, frameLen);
    frameLen++;
#endif

    return UARTCONTEXT->send(dataBuf, frameLen);
}

当界面上有个按键按下的时候可以操作:

BYTE mode[] = { 0x01, 0x02, 0x03, 0x04 };
sendProtocol(0x01, mode, 4);
powered by Gitbooklast modified: 2020-09-28 09:54:03

results matching ""

    No results matching ""