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

對于Windows來說,串口設備端口號一般都是COMn
,這里COM8使用CH340串口芯片來進行通信
對于Linux設備(這里以Ubuntu為例)
ls /dev/ttyUSB*
返回的就是你所有串口設備的設備號,如果你具有多個設備,則可以通過拔插之后,查看哪個設備增刪來確定你的設備名
問題
- 針對Ubuntu22.04可能出現(xiàn)找不到設備端口號的問題,解決方法如下:
Ubuntu22.04沒有ttyUSB(無法訪問 ‘/dev/ttyUSB‘: 沒有那個文件或目錄)問題解決
- 針對windows找不到端口,一般來說都是驅動問題,解決方法可參考下面博客
筆記本W10找不到端口(com口)及單片機串口連接的問題(附51開發(fā)板的CH340串口芯片的驅動程序安裝包)
例子(簡單版)
找到了設備的端口號之后,就可以使用python來進行通信了。
我貼一個csdn上搜python串口通信的的第一個博客的代碼例子。
代碼來源:
https://blog.csdn.net/weixin_43217958/article/details/109782000
import serial#導入串口通信庫
from time import sleepser = serial.Serial()def port_open_recv():#對串口的參數(shù)進行配置ser.port='com3'ser.baudrate=9600ser.bytesize=8ser.stopbits=1ser.parity="N"#奇偶校驗位ser.open()if(ser.isOpen()):print("串口打開成功!")else:print("串口打開失敗!")
#isOpen()函數(shù)來查看串口的開閉狀態(tài)def port_close():ser.close()if(ser.isOpen()):print("串口關閉失敗!")else:print("串口關閉成功!")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)#起到一個延時的效果,這里如果不加上一個while True,程序執(zhí)行一次就自動跳出了
但是,這個例子,并不適合直接拿來到嵌入式中進一步開發(fā),因為一些重要的參數(shù)和解包的方法并沒有說明出來,并且在python中,涉及一些數(shù)據(jù)類型的轉換也是比較復雜的(也是讓我無數(shù)次抓狂的地方😡)。
詳細使用
針對串口通信,我們可以將其分為發(fā)包和收包。而想要比較方便的使用python來實現(xiàn)通信,我們需要使用到python一個非常強大的功能包struct
,使用的講解流程,我將按照官網參數(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 – 奇偶校驗,可能的參數(shù)值:
PARITY_NONE
,PARITY_EVEN
,PARITY_ODD
,PARITY_MARK
,PARITY_SPACE
- stopbits – 停止位的比特數(shù). 可能的參數(shù)值:
STOPBITS_ONE
,STOPBITS_ONE_POINT_FIVE
,STOPBITS_TWO
- timeout (float) – 設置pyserial持續(xù)讀取數(shù)據(jù)的最長時間(s)
- xonxoff (bool) – 是否啟動軟件流控制
- rtscts (bool) – 是否啟動硬件(RTS/CTS)流控制
- dsrdtr (bool) – 是否啟動硬件(DSR/DTR)流控制
- write_timeout (float) – 設置pyserial最長寫入串口數(shù)據(jù)的時間(s)
- inter_byte_timeout (float) – 字符間超時, 沒有則禁止(默認禁止).
- exclusive (bool) – 設置獨占訪問模式(僅POSIX)。 如果端口已經以獨占訪問模式打開,則不能以獨占訪問模式打開端口。
異常退出
- ValueError –如果一些參數(shù)不在允許參數(shù)內,則返回
ValueErro
,比如波特率設置 - SerialException – 如果設備無法被找到或者被設置,則返回
SerialException
說明
當我們初始化串口的時候,open()
函數(shù)會被調用,串口就會被打開。
timeout
參數(shù)會影響到read()
函數(shù)的使用,這個timeout
參數(shù)非常重要,直接影響到我們對串口數(shù)據(jù)的讀取。
timeout
=None
: 一直等待,直到設置的接收字節(jié)數(shù)滿后退出
timeout
=0
: 非阻塞模式,在任何情況下都立即返回,返回零或更多,最多為請求的字節(jié)數(shù)
timeout
=x
:當請求的字節(jié)數(shù)可用時,將timeout設置為x秒(允許浮動)立即返回,否則等待超時到期,并返回在此之前收到的所有字節(jié)。
而對于wrtie()
(發(fā)包函數(shù))而言,默認為阻塞,除非設置了write_timeout。
針對硬件流控制而言,這個得觀察嵌入式設備了,我之前使用stm32和python通信的時候使用過一次,得需要結合硬件連接和原理圖說明,但是我并沒有完全搞透,且其他時候用的也比較少,這里就不展開敘述了。
發(fā)包
在嵌入式中,我們使用發(fā)包,一般是將我們的狀態(tài)數(shù)據(jù),或者是控制指令通過轉碼為符合設備的通信協(xié)議的格式后,將其發(fā)出。
因此,我們在編寫發(fā)包函數(shù)前,需要先熟讀通信協(xié)議,并理解我們需要發(fā)送什么樣的指令,一般協(xié)議是16進制的一串數(shù)據(jù)。
pyserial
中發(fā)包函數(shù)為write()
write(data):
參數(shù)
: 需要發(fā)送的數(shù)據(jù)
返回值
: 寫入的字節(jié)數(shù)
返回值類型
: int
異常值返回
– 如果為端口配置了寫入超時并且超過了時間。
將字節(jié)數(shù)據(jù)寫入端口。這應該是字節(jié)類型(或兼容的,如bytearray
,memoryview
)。必須對Unicode字符串進行編碼(例如“hello”.encode(“utf-8”)。
下面是參考實例
# 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ù)你的需求進行修改
send_data = [0xA4,0x03,0x08,0x23,0xD2] #需要發(fā)送的串口包send_data=struct.pack("%dB"%(len(send_data)),*send_data) #解析成16進制
print(send_data)
Usart.write(send_data) #發(fā)送
收包
針對收包,我們一般流程就是 收包
-> 檢查包
-> 解包
,先查看官網中收包的函數(shù)文檔
read(size=1)
參數(shù)
: size – 讀取字節(jié)數(shù)
返回值
: 串口讀取得到的字節(jié)
返回值類型
: bytes
從串行端口讀取字節(jié)。如果設置了超時,則返回的字符數(shù)可能少于請求的字符數(shù)。如果沒有超時,它將阻塞,直到讀取請求的字節(jié)數(shù)。
read_until(expected=LF, size=None)
參數(shù)
: expected
– 預期需要的字節(jié)。 size
– 讀多少字節(jié)。返回值
: 串口讀取到的字節(jié)數(shù)
返回值類型
: bytes
讀取數(shù)據(jù),直到找到預期的序列(默認為“\n”)、超過大小或超時。如果設置了超時,則返回的字符可能少于請求的字符。如果沒有超時,它將阻塞,直到讀取請求的字節(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ù)的時候,會按照一個timeout周期時間讀取數(shù)據(jù)# 波特率115200返回數(shù)據(jù)時間大概是1ms,9600下大概是10ms# 所以讀取時間設置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個寄存器數(shù)據(jù),即40個字節(jié)# Read_data() # 前面兩行可以注釋,換成后面這個函數(shù)
except KeyboardInterrupt:if serial != None:print("close serial port")serial.close()#--------------------------------------------------------
這里如果沒有在0.001s的時間內讀到40字節(jié)的包,就會退出。因此我們需要結合通信協(xié)議來判斷我們收到的包是否正確。
檢查包并解包
這里根據(jù)我之前使用的一款IMU(GY-95T)為例子,來演示用狀態(tài)機判斷包是否正確。
同時在函數(shù)中,我也將解包函數(shù)放入,后面再說明
def Read_data(self):'''Author: Liu Yuxiang Time: 2022.12.13description: 讀取串口數(shù)據(jù)'''# 初始化數(shù)據(jù)counter = 0 Recv_flag = 0Read_buffer = []# 接收數(shù)據(jù)至緩存區(qū)Read_buffer=serial.read(40) # 我們需要讀取的是40個寄存器數(shù)據(jù),即40個字節(jié)# 狀態(tài)機判斷收包數(shù)據(jù)是否準確while(1):# 第1幀是否是幀頭ID 0xA4if (counter == 0):if(Read_buffer[0] != 0xA4):break # 第2幀是否是讀功能碼 0x03 elif (counter == 1):if(Read_buffer[1] != 0x03):counter=0break# 第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ù)肯定錯了len=Read_buffer[3]else:counter=0break else:if(len+5==counter):#print('Recv done!')Recv_flag=1# 收包完畢if(Recv_flag):Recv_flag = 0sum = 0#print(Read_buffer) # Read_buffer中的是byte數(shù)據(jù)字節(jié)流,用struct包解包data_inspect = str(binascii.b2a_hex(Read_buffer)) # data是將數(shù)據(jù)轉化為原本的按照16進制的數(shù)據(jù)try: # 如果接收數(shù)據(jù)無誤,則執(zhí)行數(shù)據(jù)解算操作for i in range(2,80,2): # 根據(jù)手冊,檢驗所有幀之和低八位是否等于末尾幀sum += int(data_inspect[i:i+2],16)if (str(hex(sum))[-2:] == data_inspect[80:82]): # 如果數(shù)據(jù)檢驗沒有問題,則進入解包過程#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^2ACC_Y = unpack_data[1]/2048 * 9.8 ACC_Z = unpack_data[2]/2048 * 9.8```except:print("Have Error in receiving data!!")counter=0 breakelse:counter += 1 # 遍歷整個接收數(shù)據(jù)的buffer
而解包函數(shù),最關鍵的就是unpack_data = struct.unpack('<hhhhhhhhhBhhhhhhhh',Read_buffer[4:-1])
這一行代碼的含義就是將Read_buffer[4:-1]
的這部分數(shù)據(jù)按照小端的順序,解析成hhhhhhhhhBhhhhhhhh
的排列順序,h代表short
類型,B代表unsigned char
類型。
具體怎么根據(jù)你的情況使用,就可以看一下python的struct
模塊。
python struct模塊
tips:
一個byte
=8bit
\xa4 這樣一個數(shù)據(jù)代表了16bit
這部分內容是我在學習過程中借鑒很多網上寫的好的帖子而做的筆記,具體的帖子鏈接由于做筆記而找不著了,就把自己的筆記貼出來
Python沒有專門處理字節(jié)的數(shù)據(jù)類型。但由于b'str'
可以表示字節(jié)即bytes
,所以,字節(jié)數(shù)組=二進制str。在C語言中,我們可以很方便地用struct、union來處理字節(jié),以及字節(jié)和int,float的轉換。
而在python中卻會比較的麻煩,但是python是提供了struct
模塊來解決bytes
和其他二進制數(shù)據(jù)類型的轉換
struct模塊的兩個函數(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ù)轉換為什么類型的數(shù)據(jù),而這個format主要有兩部分組成,指定打包數(shù)據(jù)的方式+數(shù)據(jù)轉換后的類型,具體的表格參考如下:
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é)存放在較高的存儲器地址中,較高的有效字節(jié)存放在較低的存儲器地址 | 較高的有效字節(jié)存放在較高的存儲器地址中,較低的有效字節(jié)存放在較低的存儲器地址 |
比如數(shù)據(jù)是0x1234
低字節(jié)是0x12 高字節(jié)是0x34
小端讀出來0x34 0x12 大端讀出來就是0x12 0x34
而第二個部分也就是后面的數(shù)據(jù)要轉換成什么類型。
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) |
這么說會有點抽象,
用具體的例子來看就是
import struct
struct.pack('>I', 10240099)>>> b'\x00\x9c@c'
意思就是說,使用>
代表的字節(jié)順序即big-endian
(網絡序),來將后面的數(shù)據(jù)轉化成I
代表的數(shù)據(jù)類型(4字節(jié)無符號整型)
(官網例子)
>>>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
這里和前面的例子是一個道理,使用>
對齊的方式,來將后面三個數(shù)據(jù)依次打包成b
,h
,l
所代表的數(shù)據(jù),解包也是這樣,將一串byte
數(shù)據(jù)按照>
對齊格式轉換成b
h
l
對對應的數(shù)據(jù)。
注意:可以使用b4這樣的寫法來代表有一個數(shù)組
計算所選擇的數(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.
結語
差不多這樣就可以比較優(yōu)雅的利用python接收和讀取串口數(shù)據(jù)了。如有問題,歡迎評論區(qū)提出。