自定义的通信协议
- 自定义一主多从串口通讯_1
- 硬件基础
- 两个从机的 Tx 是相互连接的,导致一个从机在需要发送数据时发不出去了
- 协议思路
- 数据包封装和解封装
- 树莓派python串口的使用注意
- 更改树莓派串口设备驱动
- 关闭控制台功能
- python串口的使用
- 通讯应答测试
自定义一主多从串口通讯_1
这是一个类似开发日志的小记录,这篇主要是记录自己在毕设里自定义的一种基于串口通讯的通讯协议,至于为什么没有使用modbus只是因为我没有用过。
硬件基础
我的毕设大体上需要通讯的主要有三个模块分别是:树莓派当做主要的控制板、传感器采集板、动力驱动板。后面两个是自己用stm32f103rct6做的板子,以后有机会我也会展示一下
在连接上,我们知道树莓派只有一个串口是引脚引出的,所以我希望可以通过一个串口同时控制两个stm32的板子,所以一主多从的自定义串口通讯协议想法就这样出来了。
首先要解决连线问题:我们知道两个设备的串口在连线时Rx、Tx是需要反接的,但是在两个从机的时候就需要解决一个小问题:
两个从机的 Tx 是相互连接的,导致一个从机在需要发送数据时发不出去了
通过查资料我们知道,想mcu的串口都是以低电平为有效值,所以问题这就转换成为 当从机1的Tx变为低电平时如何能不受从机2 Tx高电平的影响。
显而易见,用一个二极管应该就可以解决,我直接上框图应该就清楚了:
当从机1 发送数据时Tx低电平会拉低主机Rx,同时从机2 Tx保持高电平不影响通讯。
协议思路
我的协议主要包含以下6个内容:
- 起始标志:
我这里是直接用 '>' 的值来表示
- 目标地址:
一共有三个模块所以地址也有三个
enum ModuleAddrees {
kRaspberryAddr = 0x1,
kChuanGanQiAddr = 0x2,
kQuDongBanAddr = 0x3,
kReseve1 = 0x4,
kReseve2 = 0x5,
kReseve3 = 0x6,
kReseve4 = 0x7,
}; ///< 标记模块地址
- 消息类型:
用来标记当前数据包属于那种数据类型,目前只想到了以下几个
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
}; ///< 标记数据类型
- 内容长度 :
就是简单记录消息内容的长度,主要是想用来校验,后来好像没用上,太懒了直接没写校验
- 内容:
就是要携带的主要内容
- 结束标志:
惯用换行组合 “\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调用库来实现这样的功能,希望确保数据的准确
通讯应答测试
为了方便模拟消息的应答,从机我是直接插到了串口助手上,这样我就可以控制从机发送。
测试思路是这样的,树莓派向从机发送应答命令包从机回复,如果树莓派可以正常发送和接收在控制台上会有消息显示,如果正常即为成功
- 编写测试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
- 输入指令发送并检查
- 查看控制台回复内容
- 另外也测试了其他错误的数据回复包,在设定中的错误控制台也会做出对应的反映。
至此,自定义的通讯协议初步完成了点对点的实验,明天开始测试对两个从机的通讯响应。