前言
這幾年,自己也做了一些嵌入式機(jī)器人。在整個(gè)開發(fā)的過程中,調(diào)通信通常會(huì)花費(fèi)一段比較長的時(shí)間,串口通信就是這樣的一個(gè)部分。
而現(xiàn)在在百度上進(jìn)行搜索,發(fā)現(xiàn)對python串口通信的博客講解,都有點(diǎn)太籠統(tǒng)了,這其中,應(yīng)該與python在處理硬件底層速度較慢導(dǎo)致用的人少有關(guān)系。
這里把python串口通信的部分進(jìn)行一下個(gè)人使用過程中的總結(jié)。既是自我總結(jié),也讓未來開發(fā)更快。
文章參考官方文檔:
https://pyserial.readthedocs.io/
python進(jìn)行串口通信,依賴的包就是pyserial
,因此,本文是基于這個(gè)包進(jìn)行總結(jié)。
總結(jié)過程中難免有理解不到位,或者錯(cuò)誤的地方,歡迎大家提出問題并一起交流
安裝pyserial
pip安裝
python -m pip install pyserial
# 或者直接pip安裝
pip install pyserial
conda安裝
conda install pyserial
#or
conda install -c conda-forge pyserial
pyserial大致概括
整體流程
對于串口通信,我們的大致流程如下:
硬件連接
發(fā)包
收包
硬件連接
首先需要將串口設(shè)備與電腦相連,并查看自己的串口的端口號(hào)是多少
串口的端口號(hào)就是將你的串口命名,讓程序或者系統(tǒng)能夠快捷的尋找
對于windows設(shè)備
打開設(shè)備管理器 -> 端口 -> 尋找你的設(shè)備號(hào)

對于Windows來說,串口設(shè)備端口號(hào)一般都是COMn
,這里COM8使用CH340串口芯片來進(jìn)行通信
對于Linux設(shè)備(這里以Ubuntu為例)
ls /dev/ttyUSB*
返回的就是你所有串口設(shè)備的設(shè)備號(hào),如果你具有多個(gè)設(shè)備,則可以通過拔插之后,查看哪個(gè)設(shè)備增刪來確定你的設(shè)備名
問題
- 針對Ubuntu22.04可能出現(xiàn)找不到設(shè)備端口號(hào)的問題,解決方法如下:
Ubuntu22.04沒有ttyUSB(無法訪問 ‘/dev/ttyUSB‘: 沒有那個(gè)文件或目錄)問題解決
- 針對windows找不到端口,一般來說都是驅(qū)動(dòng)問題,解決方法可參考下面博客
筆記本W(wǎng)10找不到端口(com口)及單片機(jī)串口連接的問題(附51開發(fā)板的CH340串口芯片的驅(qū)動(dòng)程序安裝包)
例子(簡單版)
找到了設(shè)備的端口號(hào)之后,就可以使用python來進(jìn)行通信了。
我貼一個(gè)csdn上搜python串口通信的的第一個(gè)博客的代碼例子。
代碼來源:
https://blog.csdn.net/weixin_43217958/article/details/109782000
import serial#導(dǎo)入串口通信庫
from time import sleep
ser = serial.Serial()
def port_open_recv():#對串口的參數(shù)進(jìn)行配置
ser.port='com3'
ser.baudrate=9600
ser.bytesize=8
ser.stopbits=1
ser.parity="N"#奇偶校驗(yàn)位
ser.open()
if(ser.isOpen()):
print("串口打開成功!")
else:
print("串口打開失?。?)
#isOpen()函數(shù)來查看串口的開閉狀態(tài)
def port_close():
ser.close()
if(ser.isOpen()):
print("串口關(guān)閉失??!")
else:
print("串口關(guān)閉成功!")
def send(send_data):
if(ser.isOpen()):
ser.write(send_data.encode('utf-8'))#編碼
print("發(fā)送成功",send_data)
else:
print("發(fā)送失??!")
if __name__ == '__main__':
port_open_recv()
while True:
a=input("輸入要發(fā)送的數(shù)據(jù):")
send(a)
sleep(0.5)#起到一個(gè)延時(shí)的效果,這里如果不加上一個(gè)while True,程序執(zhí)行一次就自動(dòng)跳出了
但是,這個(gè)例子,并不適合直接拿來到嵌入式中進(jìn)一步開發(fā),因?yàn)橐恍┲匾膮?shù)和解包的方法并沒有說明出來,并且在python中,涉及一些數(shù)據(jù)類型的轉(zhuǎn)換也是比較復(fù)雜的(也是讓我無數(shù)次抓狂的地方??)。
詳細(xì)使用
針對串口通信,我們可以將其分為發(fā)包和收包。而想要比較方便的使用python來實(shí)現(xiàn)通信,我們需要使用到python一個(gè)非常強(qiáng)大的功能包struct
,使用的講解流程,我將按照官網(wǎng)參數(shù)文檔介紹
- > 例子
這樣的順序來解釋。
serial初始化參數(shù)
class serial.Serial
init(port=None, baudrate=9600, bytesize=EIGHTBITS, parity=PARITY_NONE, stopbits=STOPBITS_ONE, timeout=None, xonxoff=False, rtscts=False, write_timeout=None, dsrdtr=False, inter_byte_timeout=None, exclusive=None)
參數(shù)
- port – 串口名字(COMn或者/dev/ttyUSB)或者
None
- baudrate (int) – 波特率,比如9600或者115200
- bytesize – 數(shù)據(jù)位數(shù),可能的參數(shù)值有:
FIVEBITS
,SIXBITS
,SEVENBITS
,EIGHTBITS
- parity – 奇偶校驗(yàn),可能的參數(shù)值:
PARITY_NONE
,PARITY_EVEN
,PARITY_ODD
,PARITY_MARK
,PARITY_SPACE
- stopbits – 停止位的比特?cái)?shù). 可能的參數(shù)值:
STOPBITS_ONE
,STOPBITS_ONE_POINT_FIVE
,STOPBITS_TWO
- timeout (float) – 設(shè)置pyserial持續(xù)讀取數(shù)據(jù)的最長時(shí)間(s)
- xonxoff (bool) – 是否啟動(dòng)軟件流控制
- rtscts (bool) – 是否啟動(dòng)硬件(RTS/CTS)流控制
- dsrdtr (bool) – 是否啟動(dòng)硬件(DSR/DTR)流控制
- write_timeout (float) – 設(shè)置pyserial最長寫入串口數(shù)據(jù)的時(shí)間(s)
- inter_byte_timeout (float) – 字符間超時(shí), 沒有則禁止(默認(rèn)禁止).
- exclusive (bool) – 設(shè)置獨(dú)占訪問模式(僅POSIX)。 如果端口已經(jīng)以獨(dú)占訪問模式打開,則不能以獨(dú)占訪問模式打開端口。
異常退出
- ValueError –如果一些參數(shù)不在允許參數(shù)內(nèi),則返回
ValueErro
,比如波特率設(shè)置 - SerialException – 如果設(shè)備無法被找到或者被設(shè)置,則返回
SerialException
說明
當(dāng)我們初始化串口的時(shí)候,open()
函數(shù)會(huì)被調(diào)用,串口就會(huì)被打開。
timeout
參數(shù)會(huì)影響到read()
函數(shù)的使用,這個(gè)timeout
參數(shù)非常重要,直接影響到我們對串口數(shù)據(jù)的讀取。
timeout
=None
: 一直等待,直到設(shè)置的接收字節(jié)數(shù)滿后退出timeout
=0
: 非阻塞模式,在任何情況下都立即返回,返回零或更多,最多為請求的字節(jié)數(shù)timeout
=x
:當(dāng)請求的字節(jié)數(shù)可用時(shí),將timeout設(shè)置為x秒(允許浮動(dòng))立即返回,否則等待超時(shí)到期,并返回在此之前收到的所有字節(jié)。
而對于wrtie()
(發(fā)包函數(shù))而言,默認(rèn)為阻塞,除非設(shè)置了write_timeout。
針對硬件流控制而言,這個(gè)得觀察嵌入式設(shè)備了,我之前使用stm32和python通信的時(shí)候使用過一次,得需要結(jié)合硬件連接和原理圖說明,但是我并沒有完全搞透,且其他時(shí)候用的也比較少,這里就不展開敘述了。
發(fā)包
在嵌入式中,我們使用發(fā)包,一般是將我們的狀態(tài)數(shù)據(jù),或者是控制指令通過轉(zhuǎn)碼為符合設(shè)備的通信協(xié)議的格式后,將其發(fā)出。
因此,我們在編寫發(fā)包函數(shù)前,需要先熟讀通信協(xié)議,并理解我們需要發(fā)送什么樣的指令,一般協(xié)議是16進(jìn)制的一串?dāng)?shù)據(jù)。pyserial
中發(fā)包函數(shù)為write()
write(data):
參數(shù)
: 需要發(fā)送的數(shù)據(jù)返回值
: 寫入的字節(jié)數(shù)返回值類型
: int異常值返回
– 如果為端口配置了寫入超時(shí)并且超過了時(shí)間。
將字節(jié)數(shù)據(jù)寫入端口。這應(yīng)該是字節(jié)類型(或兼容的,如bytearray
,memoryview
)。必須對Unicode字符串進(jìn)行編碼(例如“hello”.encode(“utf-8”)。
下面是參考實(shí)例
# Usart Library
import serial
import struct
import binascii
# Init serial port
Usart = serial.Serial(
port = '/dev/ttyUSB0', # 串口
baudrate=115200, # 波特率
timeout = 0.001 )
# 判斷串口是否打開成功
if Usart.isOpen():
print("open success")
else:
print("open failed")
# 使用優(yōu)雅的方式發(fā)送串口數(shù)據(jù)
# 這里的數(shù)據(jù)可以根據(jù)你的需求進(jìn)行修改
send_data = [0xA4,0x03,0x08,0x23,0xD2] #需要發(fā)送的串口包
send_data=struct.pack("%dB"%(len(send_data)),*send_data) #解析成16進(jìn)制
print(send_data)
Usart.write(send_data) #發(fā)送
收包
針對收包,我們一般流程就是 收包
-> 檢查包
-> 解包
,先查看官網(wǎng)中收包的函數(shù)文檔
read(size=1)
參數(shù)
: size – 讀取字節(jié)數(shù)
返回值
: 串口讀取得到的字節(jié)
返回值類型
: bytes
從串行端口讀取字節(jié)。如果設(shè)置了超時(shí),則返回的字符數(shù)可能少于請求的字符數(shù)。如果沒有超時(shí),它將阻塞,直到讀取請求的字節(jié)數(shù)。
read_until(expected=LF, size=None)
參數(shù)
: expected
– 預(yù)期需要的字節(jié)。 size
– 讀多少字節(jié)。返回值
: 串口讀取到的字節(jié)數(shù)
返回值類型
: bytes
讀取數(shù)據(jù),直到找到預(yù)期的序列(默認(rèn)為“\n”)、超過大小或超時(shí)。如果設(shè)置了超時(shí),則返回的字符可能少于請求的字符。如果沒有超時(shí),它將阻塞,直到讀取請求的字節(jié)數(shù)。
in_waiting()
獲得input buffer中緩存字節(jié)數(shù)返回值類型
: int
收包
我們首先需要從串口中讀取緩存數(shù)據(jù)
# Usart Library
import serial
import struct
import binascii
# Init serial port
Usart = serial.Serial(
port = '/dev/ttyUSB0', # 串口
baudrate=115200, # 波特率
timeout = 0.001 ) # 由于后續(xù)使用read在未收全數(shù)據(jù)的時(shí)候,會(huì)按照一個(gè)timeout周期時(shí)間讀取數(shù)據(jù)
# 波特率115200返回?cái)?shù)據(jù)時(shí)間大概是1ms,9600下大概是10ms
# 所以讀取時(shí)間設(shè)置0.001s
# 判斷串口是否打開成功
if Usart.isOpen():
print("open success")
else:
print("open failed")
# ----讀取串口數(shù)據(jù)-----------------------------------
try:
count = serial.inWaiting()
if count > 0:
# 初始化數(shù)據(jù)
Read_buffer = []
# 接收數(shù)據(jù)至緩存區(qū)
Read_buffer=serial.read(40) # 我們需要讀取的是40個(gè)寄存器數(shù)據(jù),即40個(gè)字節(jié)
# Read_data() # 前面兩行可以注釋,換成后面這個(gè)函數(shù)
except KeyboardInterrupt:
if serial != None:
print("close serial port")
serial.close()
#--------------------------------------------------------
這里如果沒有在0.001s的時(shí)間內(nèi)讀到40字節(jié)的包,就會(huì)退出。因此我們需要結(jié)合通信協(xié)議來判斷我們收到的包是否正確。
檢查包并解包
這里根據(jù)我之前使用的一款I(lǐng)MU(GY-95T)為例子,來演示用狀態(tài)機(jī)判斷包是否正確。
同時(shí)在函數(shù)中,我也將解包函數(shù)放入,后面再說明
def Read_data(self):
'''
Author: Liu Yuxiang
Time: 2022.12.13
description: 讀取串口數(shù)據(jù)
'''
# 初始化數(shù)據(jù)
counter = 0
Recv_flag = 0
Read_buffer = []
# 接收數(shù)據(jù)至緩存區(qū)
Read_buffer=serial.read(40) # 我們需要讀取的是40個(gè)寄存器數(shù)據(jù),即40個(gè)字節(jié)
# 狀態(tài)機(jī)判斷收包數(shù)據(jù)是否準(zhǔn)確
while(1):
# 第1幀是否是幀頭ID 0xA4
if (counter == 0):
if(Read_buffer[0] != 0xA4):
break
# 第2幀是否是讀功能碼 0x03
elif (counter == 1):
if(Read_buffer[1] != 0x03):
counter=0
break
# 第3幀判斷起始幀
elif (counter == 2):
if(Read_buffer[2] < 0x2c):
start_reg=Read_buffer[2]
else:
counter=0
# 第4幀判斷幀有多少數(shù)量
elif (counter == 3):
if((start_reg+Read_buffer[3]) < 0x2C): # 最大寄存器為2C 大于0x2C說明數(shù)據(jù)肯定錯(cuò)了
len=Read_buffer[3]
else:
counter=0
break
else:
if(len+5==counter):
#print('Recv done!')
Recv_flag=1
# 收包完畢
if(Recv_flag):
Recv_flag = 0
sum = 0
#print(Read_buffer) # Read_buffer中的是byte數(shù)據(jù)字節(jié)流,用struct包解包
data_inspect = str(binascii.b2a_hex(Read_buffer)) # data是將數(shù)據(jù)轉(zhuǎn)化為原本的按照16進(jìn)制的數(shù)據(jù)
try: # 如果接收數(shù)據(jù)無誤,則執(zhí)行數(shù)據(jù)解算操作
for i in range(2,80,2): # 根據(jù)手冊,檢驗(yàn)所有幀之和低八位是否等于末尾幀
sum += int(data_inspect[i:i+2],16)
if (str(hex(sum))[-2:] == data_inspect[80:82]): # 如果數(shù)據(jù)檢驗(yàn)沒有問題,則進(jìn)入解包過程
#print('the Rev data is right')
# 數(shù)據(jù)低八位在前,高八位在后
#print(Read_buffer[4:-1])
unpack_data = struct.unpack('<hhhhhhhhhBhhhhhhhh',Read_buffer[4:-1])
# 切片并將其解析為我們所需要的數(shù)據(jù),切出我們所需要的數(shù)據(jù)部分
```
比如:
ACC_X = unpack_data[0]/2048 * 9.8 # unit m/s^2
ACC_Y = unpack_data[1]/2048 * 9.8
ACC_Z = unpack_data[2]/2048 * 9.8
```
except:
print("Have Error in receiving data!!")
counter=0
break
else:
counter += 1 # 遍歷整個(gè)接收數(shù)據(jù)的buffer
而解包函數(shù),最關(guān)鍵的就是unpack_data = struct.unpack('<hhhhhhhhhBhhhhhhhh',Read_buffer[4:-1])
這一行代碼的含義就是將Read_buffer[4:-1]
的這部分?jǐn)?shù)據(jù)按照小端的順序,解析成hhhhhhhhhBhhhhhhhh
的排列順序,h代表short
類型,B代表unsigned char
類型。
具體怎么根據(jù)你的情況使用,就可以看一下python的struct
模塊。
python struct模塊
tips:
一個(gè)byte
=8bit
\xa4 這樣一個(gè)數(shù)據(jù)代表了16bit
這部分內(nèi)容是我在學(xué)習(xí)過程中借鑒很多網(wǎng)上寫的好的帖子而做的筆記,具體的帖子鏈接由于做筆記而找不著了,就把自己的筆記貼出來
Python沒有專門處理字節(jié)的數(shù)據(jù)類型。但由于b'str'
可以表示字節(jié)即bytes
,所以,字節(jié)數(shù)組=二進(jìn)制str。在C語言中,我們可以很方便地用struct、union來處理字節(jié),以及字節(jié)和int,float的轉(zhuǎn)換。
而在python中卻會(huì)比較的麻煩,但是python是提供了struct
模塊來解決bytes
和其他二進(jìn)制數(shù)據(jù)類型的轉(zhuǎn)換
struct模塊的兩個(gè)函數(shù),struct.unpack
和struct.pack
函數(shù)
struct.pack(format, v1, v2, …)
Return a bytes object containing the values v1, v2, … packed according to the format string format. The arguments must match the values required by the format exactly.
struct.unpack(format, buffer)
Unpack from the buffer buffer (presumably packed by
pack(format, ...)
) according to the format string format. The result is a tuple even if it contains exactly one item. The buffer’s size in bytes must match the size required by the format, as reflected bycalcsize()
.https://docs.python.org/3/library/struct.html#module-struct
format也就是你需要將后面的數(shù)據(jù)轉(zhuǎn)換為什么類型的數(shù)據(jù),而這個(gè)format主要有兩部分組成,指定打包數(shù)據(jù)的方式+數(shù)據(jù)轉(zhuǎn)換后的類型,具體的表格參考如下:
format可用于指示打包數(shù)據(jù)的字節(jié)順序、大小和對齊方式,如下表所示:
Character | Byte order | Size | Alignment |
---|---|---|---|
@ |
native | native | native |
= |
native | standard | none |
< |
little-endian | standard | none |
> |
big-endian | standard | none |
! |
network (= big-endian) | standard | none |
大端(big-endian) | 小端(little-endian) |
---|---|
較低的有效字節(jié)存放在較高的存儲(chǔ)器地址中,較高的有效字節(jié)存放在較低的存儲(chǔ)器地址 | 較高的有效字節(jié)存放在較高的存儲(chǔ)器地址中,較低的有效字節(jié)存放在較低的存儲(chǔ)器地址 |
比如數(shù)據(jù)是0x1234
低字節(jié)是0x12 高字節(jié)是0x34
小端讀出來0x34 0x12 大端讀出來就是0x12 0x34
而第二個(gè)部分也就是后面的數(shù)據(jù)要轉(zhuǎn)換成什么類型。
Format | C Type | Python type | Standard size(byte) | Notes |
---|---|---|---|---|
x |
pad byte | no value | (7) | |
c |
char | bytes of length 1 | 1 | |
b |
signed char | integer | 1 | (1), (2) |
B |
unsigned char | integer | 1 | (2) |
? |
_Bool | bool | 1 | (1) |
h |
short | integer | 2 | (2) |
H |
unsigned short | integer | 2 | (2) |
i |
int | integer | 4 | (2) |
I |
unsigned int | integer | 4 | (2) |
l |
long | integer | 4 | (2) |
L |
unsigned long | integer | 4 | (2) |
q |
long long | integer | 8 | (2) |
Q |
unsigned long long | integer | 8 | (2) |
n |
ssize_t | integer | (3) | |
N |
size_t | integer | (3) | |
e |
(6) | float | 2 | (4) |
f |
float | float | 4 | (4) |
d |
double | float | 8 | (4) |
s |
char[] | bytes | (9) | |
p |
char[] | bytes | (8) | |
P |
void* | integer | (5) |
這么說會(huì)有點(diǎn)抽象,
用具體的例子來看就是
import struct
struct.pack('>I', 10240099)
>>> b'\x00\x9c@c'
意思就是說,使用>
代表的字節(jié)順序即big-endian
(網(wǎng)絡(luò)序),來將后面的數(shù)據(jù)轉(zhuǎn)化成I
代表的數(shù)據(jù)類型(4字節(jié)無符號(hào)整型)
(官網(wǎng)例子)
>>>from struct import *
>>>pack(">bhl", 1, 2, 3)
b'\x01\x00\x02\x00\x00\x00\x03'
>>>unpack('>bhl', b'\x01\x00\x02\x00\x00\x00\x03')
(1, 2, 3)
>>>calcsize('>bhl')
7
這里和前面的例子是一個(gè)道理,使用>
對齊的方式,來將后面三個(gè)數(shù)據(jù)依次打包成b
,h
,l
所代表的數(shù)據(jù),解包也是這樣,將一串byte
數(shù)據(jù)按照>
對齊格式轉(zhuǎn)換成b
h
l
對對應(yīng)的數(shù)據(jù)。
注意:可以使用b4這樣的寫法來代表有一個(gè)數(shù)組
計(jì)算所選擇的數(shù)據(jù)格式有多少字節(jié)
struct.calcsize(format)
Return the size of the struct (and hence of the bytes object produced by
pack(format, ...)
) corresponding to the format string format.文章來源:http://www.zghlxwxcb.cn/news/detail-472030.html
結(jié)語
差不多這樣就可以比較優(yōu)雅的利用python接收和讀取串口數(shù)據(jù)了。如有問題,歡迎評論區(qū)提出。文章來源地址http://www.zghlxwxcb.cn/news/detail-472030.html
到了這里,關(guān)于python+嵌入式——串口通信篇(收發(fā)解包)的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!