一、基于Python的串口读数
通常来说,许多传感器是通过串口进行数据传输的。串口通信(Serial Communications)的概念非常简单,串口按位(bit)发送和接收字节。尽管比按字节(byte)的并行通信慢,但是串口可以在使用一根线发送数据的同时用另一根线接收数据。它很简单并且能够实现远距离通信。串口通信使用3根线完成,分别是地线、发送、接收。由于串口通信是异步的,端口能够在一根线上发送数据同时在另一根线上接收数据。其他线用于握手,但不是必须的。串口通信最重要的参数是波特率、数据位、停止位和奇偶校验。对于两个进行通信的端口,这些参数必须匹配的[1]。
串口通信虽然简单,但是在处理如GNSS/惯导紧组合场景下,数据传输不能发生掉数、误码等现象。所以如何书写可靠的串口读数程序是必须的。对于工科本科生和工程师而言更是必备技能。本篇文章将详尽分析串口读数的工作流程,并给出基于Python的串口读数伪代码。
二、工作流程描述
- 首先应当使用“设备管理器”(Windows系统)或者Cutecom(Linux系统)查看当前的串口号。Linux系统建议将物理设备绑定至某一串口名,这样防止后续拔插的过程中串口名称改变。
- 根据设备名称,定义相应的baudrate,bytesize,timeout等。
- 在主程序中定义两个线程:接收线程、处理和解析线程。(*对于实时性要求较高的应用,也可以定义三个线程:接收、处理、解析)
- 接收线程。根据通信双方定义的串口协议以及一帧数据的字长,进行相应的数据接收。工作流程主要是不断的从串口读取数据,然后将数据放入缓存区(通常可设置为2048),然后按照通信格式一帧的长度去寻找帧头,找到后做CRC校验,校验通过后从缓存区删除对应的数据帧。完成一帧数据的读取。
- 解析线程。根据通信双方定义的串口协议,对相应的串口数据进行解析。从一堆"AA BB CC"中恢复出自己想要的数据。
三、准备工作
需要下载pyserial。若使用conda环境则直接使用:
conda install pyserial
命令即可。
首先应当定义几个超参数:
len_data: 单次读入的字节数;
REC_Length: 当前接收到数据总长度;
str_RS: 开辟的缓冲区大小,用来存放数组;
dealData_RS: 用来存放接受值的数组;
DealData_RS: 用来处理接收值的二维数组;
REC_Length_RS: 接收到的数据长度
Temp_Pos_RS: 用来记录定义串口传输协议的帧头在哪一个字节
RECPOS_RS: 接受线程处理标志位;
DealPos_RS: 处理线程标志位;
incorrect_RS: CRC校验未通过的标志位。
四、完整代码
#%% All Hyper-parameters
len_data_RS = 20
REC_Length_RS = 0
str_RS = [0 for x in range(0, 2048)]
dealData_RS = [0 for x in range(0, len_data_RS)]
Dealdata_RS = [[0 for x in range(len_data_RS)] for y in range(10)]
Temp_Pos_RS = 0
RECPOS_RS = 0
DealPOS_RS = 0
incorrect_RS = 0
right_RS = 0
# RS测量数据接收线程
def run_rec_RS():
global REC_Length_RS, Temp_Pos_RS, RECPOS_RS, incorrect_RS, right_RS, str_RS, Dealdata_RS
while True:
if ser_RS.in_waiting:
data = (bytes)(ser_RS.read(len_data_RS)) # 读进来之后,赋予的类型是Bytes
len_rec = len(data)
if data != "": # 如果读到的数据不为空
for i in range(len_rec):
str_RS[i + REC_Length_RS] = data[i] # 将读取数据放到数组中
REC_Length_RS = REC_Length_RS + len_rec
# 如果接收到的数据长度比定义的数据长度还短,那肯定没有一包信息了,返回
if REC_Length_RS<len_data_RS:
return
# 如果不是的话,去掉4个字节的CRC,找帧头先
for i in range(REC_Length_RS-3):
# 如果帧头找到了,记录下帧头在缓冲区中的位置。在这里我定义的帧头转换为十进制就是170和193
if (str_RS[i]==170) & (str_RS[i+1]==193):
Temp_Pos_RS = i
break
# 如果已经寻到了倒数第四个字节还没寻到,那么从倒数第四个字节接着往下寻。
if i == REC_Length_RS-3:
Temp_Pos_RS = REC_Length_RS-3
# 如果当前总的字节长度减去帧头索引大于数据长度
if REC_Length_RS-Temp_Pos_RS >= (len_data_RS-1):
# 那么就把数据放到二维数组中来
for j in range(len_data_RS):
Dealdata_RS[RECPOS_RS][j] = str_RS[j + Temp_Pos_RS]
# 计算CRC异或和校验
crc_data = 0
for i in range(len_data_RS-1):
crc_data = Dealdata_RS[RECPOS_RS][i] ^ crc_data
i = i+1
if crc_data != Dealdata_RS[RECPOS_RS][i]:
incorrect_RS = incorrect_RS + 1
print("Incorrect RS Flag is",incorrect_RS)
else:
RECPOS_RS = (RECPOS_RS + 1)%10
# 如果通过CRC校验和,则将数据存至数组恰当位置;否则仅抛弃帧头段
for j in range(REC_Length_RS - Temp_Pos_RS - len_data_RS):
str_RS[j] = str_RS[j + Temp_Pos_RS + len_data_RS]
REC_Length_RS = REC_Length_RS - Temp_Pos_RS - len_data_RS
else:
for j in range(REC_Length_RS - Temp_Pos_RS):
str_RS[j] = str_RS[j + Temp_Pos_RS]
REC_Length_RS = REC_Length_RS - Temp_Pos_RS
# 解析数据线程
def run_deal_RS():
global RECPOS_RS, DealPOS_RS, Dealdata_RS
while True:
dealdd = [0 for x in range(0, len_data_RS)]
# 如果处理标志位和接受标志位不同,则说明可以处理的过来。如果相同则说明有可能会发生处理处理完了但是接收还没有接收的情况,时序可能造成混乱。
if RECPOS_RS != DealPOS_RS:
if (Dealdata_RS[DealPOS_RS][0] == 170) & (Dealdata_RS[DealPOS_RS][1] == 193):
# 按照帧格式解析你的数据
if __name__ == '__main__':
try:
ser_RS = serial.Serial(
# port='/dev/ttyUSB0',
port='/COM4',
baudrate=9600,
parity='N', # 校验位
bytesize=8, # 字节大小
timeout=1
)
except SerialException:
ser_RS.close()
ser_RS = serial.Serial(
# port='/dev/ttyUSB0', # /dev/ttys0
port='/COM4',
baudrate=9600,
parity='N', # 校验位
bytesize=8, # 字节大小
timeout=1,
)
五、注意事项
整个的串口读数程序基本就是这样了。在运算过程中,有几个要注意的点:
- 发送和接收数据转换精度的问题。如果是浮点但是使用int接收,可以让发送方乘一个1e2,让浮点变成整型;
- 在实际的工程应用过程中,如果涉及串口间的信息交互,注意一点就是串口发送控制指令后可能还会从缓冲取解析一到两帧的数据。这个时候记得要做程序保护,不要因为状态的切换导致错误。
工程应用中的流程如上,如果有疑问也欢迎进行交流。
Ref:
[1]https://baike.baidu.com/item/%E4%B8%B2%E5%8F%A3%E9%80%9A%E4%BF%A1/3775296?fr=aladdin