自定义的串口通信协议

自定义一主多从串口通讯_1

这是一个类似开发日志的小记录,这篇主要是记录自己在毕设里自定义的一种基于串口通讯的通讯协议,至于为什么没有使用modbus只是因为我没有用过。

硬件基础

我的毕设大体上需要通讯的主要有三个模块分别是:树莓派当做主要的控制板、传感器采集板动力驱动板。后面两个是自己用stm32f103rct6做的板子,以后有机会我也会展示一下

在连接上,我们知道树莓派只有一个串口是引脚引出的,所以我希望可以通过一个串口同时控制两个stm32的板子,所以一主多从的自定义串口通讯协议想法就这样出来了。

首先要解决连线问题:我们知道两个设备的串口在连线时Rx、Tx是需要反接的,但是在两个从机的时候就需要解决一个小问题:

两个从机的 Tx 是相互连接的,导致一个从机在需要发送数据时发不出去了

通过查资料我们知道,想mcu的串口都是以低电平为有效值,所以问题这就转换成为 当从机1的Tx变为低电平时如何能不受从机2 Tx高电平的影响。
显而易见,用一个二极管应该就可以解决,我直接上框图应该就清楚了:
从机1 发送数据时Tx低电平会拉低主机Rx,同时从机2 Tx保持高电平不影响通讯。
在这里插入图片描述

协议思路

我的协议主要包含以下6个内容:

  1. 起始标志:我这里是直接用 '>' 的值来表示
  2. 目标地址:一共有三个模块所以地址也有三个
enum ModuleAddrees {
    kRaspberryAddr = 0x1,
    kChuanGanQiAddr = 0x2,
    kQuDongBanAddr = 0x3,
    kReseve1        = 0x4,
    kReseve2        = 0x5,
    kReseve3        = 0x6,
    kReseve4        = 0x7,
};   ///< 标记模块地址
  1. 消息类型:用来标记当前数据包属于那种数据类型,目前只想到了以下几个
enum DataTypeId {
    kAck_dti = 0x0,
    kYaw_dti = 0x1,
    kCio_dti = 0x2,
    kAdc_dti = 0x3,
    kEncoder_dti = 0x4,
    kMoto_dti = 0x5,
    kServe_dti = 0x6,
    kPower_dti = 0x7,

    kReseveLast_dti = 0x1f, //0001 1111
};   ///< 标记数据类型
  1. 内容长度 :就是简单记录消息内容的长度,主要是想用来校验,后来好像没用上,太懒了直接没写校验
  2. 内容:就是要携带的主要内容
  3. 结束标志:惯用换行组合 “\r\n”,对应数组就是[13 10]
数据包封装和解封装

基本上用一个数组把上面的内容全部接下来就可以了,不过我为了能少一个byte把目标地址消息类型的值通过移位放到了一个8位数据里了(高3位的bit表示目标地址, 低5位的bit表示消息类型)。我就直接上代码了,这样简单直接

/**
 * @brief 封装数据包
 * @param package 数据包
 * @param detAddr 目标模块地址
 * @param dti   数据类型标号
 * @param len   数据长度
 * @param srcData 原始数据(组)
 * @return 数据包长度, 构造失败返回-1
 */
int MES_PackageData(char *package, enum ModuleAddrees detAddr, enum DataTypeId dti,
                    unsigned char len, void *srcData)
{
    int i;
    int result = -1;
    char *p = package;
    unsigned char addrTid = (((unsigned char)detAddr) << 5) | ((unsigned char)dti);

    // 起始标志 ‘>’
    *p = '>';
    p++;
    // 模块号(3bit)+数据类型号(5bit)
    *p = addrTid;
    p++;
    // 数据(组)长度,单位(byte)
    *p = len;
    p++;
    // 数据(组)
    for (i = 0; i < len; i++, p++) {
        *p = ((char *)srcData)[i];
    }
    // 结束标志 “\r\n”
    *p = '\r'; p++; *p = '\n';

    // 计算包长度
    result = p - package + 1;

    return result;
}                    
/**
 * @brief 拆解数据包
 * @param package 数据包
 * @param selfAddr 本机模块号
 * @param res 获得数据
 * @param dti 数据类型编号
 * @return 数据长度
 */
int MES_UnpackageData(char *package, enum ModuleAddrees selfAddr,
                    char *res, enum DataTypeId *dti)
{
    int result = -1;
    char *p = package;
    unsigned char temp;
    int i;

    // 判断起始标志
    if (*p != '>') {
        return result;
    }
    p++;
    // 判断模块号(3bit)+获取数据类型号(5bit)
    temp = *p;
    if ((temp >> 5) != (unsigned char)selfAddr) {
        return result;
    }
    *dti = temp & 0x1f;
    p++;
    // 获取数据
    result = *p;
    p++;
    for (i = 0; i < result; i++, p++) {
        res[i] = *p;
    }

    return result;
}

以上是stm32从机的代码,下面是在树莓派上写的python代码

def __packData(self, detAddr = ModuleAddrees.kNone,
                   dti = DataTypeId.kAdc_dti,
                   len = 0, srcData = np.zeros(0,dtype = np.uint8)):
        '''
        :breif 打包数据
        :param detAddr:  目标地址
        :param dti:   数据类型
        :param len: 参数长度
        :param srcData: 参数列表
        :return: 完整数据包 np.ndarray
        '''
        pack = np.zeros(len + 5, dtype=np.uint8)
        pack[0] = ord('>') # begin flag
        pack[1] = np.uint8(detAddr << 5) | np.uint8(dti & 0x1f)
        pack[2] = np.uint8(len)
        pack[-2] = ord('\r')
        pack[-1] = ord('\n')

        i = 0
        while i < len:
            pack[i+3] = np.uint8(srcData[i])
            i += 1

        return pack

def __Unpackage(self, pack = '', len = 0):
        '''
        解开数据包
        :param pack:
        :param len:
        :return: 内容   长度   类型
        '''
        pck = np.array(list(pack), dtype=np.uint8)
        if pack.__len__() < 6:
            dataContent = []
            dataType = -1
            dataSize = -1
            return (dataContent, dataSize, dataType)

        dataContent = pck[3:-2]
        dataSize = pck[2]
        dataType = DataTypeId(pck[1] & 0x1f)

        return (dataContent, dataSize, dataType)

'''
这两函数是一个类方法,不能直接用,能看出来流程就行。

到这我的协议内容基本就结束了,后面主要是记录在第一次使用树莓派串口时候要做的工作和注意事项
'''

树莓派python串口的使用注意

更改树莓派串口设备驱动

我用的是树莓派4b,芯片内部有两个串口:一个称之为硬件串口(/dev/ttyAMA0),一个称之为mini串口(/dev/ttyS0)。两个区别是ttyAMA0是外设串口有单独的时钟,而ttyS0是简易的串口由内核提供时钟,所以AMA0会相对稳定,ttyS0会受到内核时钟的影响。

通过指令ls -l /dev ,可以看出目前引脚引出的串口是ttyS0,所以第一步的工作就是要修改这个映射关系
在这里插入图片描述
参考博客:修改树莓派串口设备映射
主要内容是找到相关文件 我这里是 “/boot/overlays/miniuart-bt.dtbo” ,不同版本系统多少会有点不一样
在这里插入图片描述
然后编辑 /boot/config.txt
在该文件中增加一行代码 dtoverlay=“对应的文件名”
在这里插入图片描述
然后保存文件,重启树莓派使之生效。
在此查看设备,对应关系就改过来了
在这里插入图片描述

关闭控制台功能

起初为了能通过串口进入系统,打开了控制台功能,这里需要关闭,两个指令即可

sudo systemctl stop serial-getty@ttyAMA0.service
sudo systemctl disable serial-getty@ttyAMA0.service

我看的博客都有讲到还需要修改 /boot/cmdline.txt
把里面关于串口的内容都删掉,但是我发现我的里面直接就已经没有了,所以有需要的转置上面的超链接

再次重启就可以正常使用了

python串口的使用

我没有使用WiringPi库,而是直接用的PySerial。在边学边用的过程中发现,python的串口数据在解析的时候不像C语言的那么方便(应该是我太菜了),我总是在纠结python如何对数据进行声明,如何对数据强制转换。
目前为止,我发现通过串口发送数组类型是相对方便的所以在python中我的串口数据包都是以数组的形式进行传递(dtype=np.uint8)。
可能是因为python中万物皆对象的概念导致我没办法实现类似C语言中union这样的相同内存不同数据类型的数据结构(应该还是我太菜了)
所以我准备后面用c语言写个静态库,python调用库来实现这样的功能,希望确保数据的准确

通讯应答测试

为了方便模拟消息的应答,从机我是直接插到了串口助手上,这样我就可以控制从机发送。
测试思路是这样的,树莓派向从机发送应答命令包从机回复,如果树莓派可以正常发送和接收在控制台上会有消息显示,如果正常即为成功

  1. 编写测试demo

def __task_enter(self,argm = []):
        '''
        对接收到的数据进行分析并且做出响应
        :return:
        '''
        lock = threading.Lock()
        try:
            while self.__TaskIsRun:
                # step.1 接收数据包
                (self.r_data, self.r_len) = self.ReadLine()
                # step.2 解析数据包
                lock.acquire()
                (cont, size, type) = self.__Unpackage(self.r_data, self.r_len)
                lock.release()
                # step.3 分析数据
                if type == DataTypeId.kAck_dti:  #解析应答回复内容
                    print("get kAck_dti\n")
                    if cont[0] == ModuleAddrees.kChuanGanQiAddr.value:
                        self.dataApi.ack_CGQ = 1
                        self.ipc.cmdAckCGQ.set()
                    elif cont[0] == ModuleAddrees.kQuDongBanAddr.value:
                        self.dataApi.ack_QDB = 1
                        self.ipc.cmdAckQDB.set()
                    else:
                        print("ack pack cont error\n")
                    pass
                elif type == DataTypeId.kMoto_dti:
                    print("get kMoto_dti\n")
                    pass
                continue
        except:
            print("The thread is end >> " + self.task.getName())
            return



if __name__ == "__main__":
    prl = SpcProtocol("protocolTest", "/dev/ttyAMA0", 115200)

    prl.StartTask()
    while True:
        key = input("输入cmd: ")
        print(key, end = '\n')
        if key == 'ack':
            prl.SendCmdAck(ModuleAddrees.kQuDongBanAddr)  # 发送应答命令
        print("wait for ack\n")
        prl.ipc.cmdAckQDB.wait()
        print('get it from QuDongBan\n')
        continue


  1. 输入指令发送并检查
    在这里插入图片描述
    在这里插入图片描述

  2. 查看控制台回复内容
    在这里插入图片描述
    在这里插入图片描述
    另外也测试了其他错误的数据回复包,在设定中的错误控制台也会做出对应的反映。
    至此,自定义的通讯协议初步完成了点对点的实验,明天开始测试对两个从机的通讯响应。

  • 1
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
实现自定义串口通信协议可以通过以下几个步骤来完成。 第一步是确定协议的基本要素,包括帧格式、数据字段和控制字段等。帧格式可选择起始位、数据位、校验位和停止位等组成,数据字段用于传输需要通信的数据,控制字段用于控制通信过程。 第二步是确定通信双方的通信流程和通信规则。通信流程包括建立连接、数据传输和断开连接等步骤,通信规则包括数据发送方和接收方之间的协作方式、错误处理方式等。 第三步是实现软件端和硬件端的通信功能。在软件端,可以使用编程语言如Python、C++等来实现自定义协议的编码和解码功能,将要发送的数据按照协议格式编码后发送,接收到的数据按照协议格式解码后进行处理。在硬件端,可以使用控制芯片如8051、Arduino等来实现串口通信的物理层功能。 第四步是进行通信测试和调试。在通信过程中,可以通过单元测试和集成测试来验证协议的正确性和可靠性,通过日志记录和错误处理来定位和解决通信中出现的问题。 最后一步是对协议进行优化和改进。根据实际需求,可以对协议的性能、安全性和扩展性等进行改进,提高通信速度、保护数据安全和支持更多的功能扩展。 总之,实现自定义串口通信协议需要考虑协议的基本要素、通信流程和通信规则,并在软硬件端进行功能实现和测试调试,最后对协议进行优化和改进。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值