UART串口通信¶
概要¶
在本節課程1Z實驗室為大家講解串口通信的接線方式,ESP32中的串口UART資源與相關API, 并給出了一個UART的小應用實例。
keywords uart communication micropython-esp32 usb2ttl
什么是串口通信¶
串口通信的英文縮寫是UART(Universal Asynchronous Receiver Transmitter) 全稱是通用異步收發器。
定義都是一些看似簡單實則難以理解的文字。
但是,聽起來很高深的概念,其實就是上面gif里的模型,兩個設備,一根線串起來,發送方在線的一頭將數據轉換為二進制序列,用高低電平按照順序依次發送01信號,接收方在線的另一頭讀取這根信號線上的高低電平信號,對應轉化為二進制的01序列。這就是最基本的串口通信的概念,本節教程接下來所講解的也是這種最基礎的串口通信。
異步收發¶
為什么叫異步收發呢?因為如果我給上圖的兩個設備再加一根線,比如下圖,讓左邊的設備也可以成為接收方,右邊的設備也可以成為發送方,那么對于左右兩個設備而言,發送和接受便可以在兩根線上同時進行,所以說,發送和接收是異步的。
波特率¶
概念¶
波特率(bandrate
)是指,每秒鐘我們的串口通信所傳輸的bit個數,通俗的講就是在一秒內能夠發送多少個1和0的二進制數。
比如,波特率是9600,就意味著1S中可以發送9600個0和1組成的二級制序列。
波特率機制潛在的問題¶
在串口通信的過程中,波特率直接影響著通信的速率,而且在發送端和接收端必須保證同樣的波特率,這樣才能在一定程度上保證發送和接收保持同步。
然而實際上在發送方和接收方波特率不匹配或者波特率較高的時候,或者有信號干擾,很可能發送和接收的順序會發生錯亂,導致數據的溢出,就像下面的動圖中展示的那樣——發送方波特率高于接收方:
暫時請忽略上面的DTR和DSR所連接的線,筆者只借此圖說明串口通信中波特率不一致或波特率極高的情況下會出現的數據丟失問題。
當然一般在9600和115200的波特率下,上面的情況幾乎不會發生,但是這種靠發送接收速率來保持數據同步的方式顯然還是存在缺陷的。
針對于波特率的改進¶
為了改進上面的問題,很多基于串口通信的協議都會加上一條時鐘信號線,信息的發送和接受,發送方和接收方都要完全按照時鐘信號線上的信號來執行,這樣,有了第三者 的介入,就使得數據發送和接受能夠完全的保證同步。
做個不恰當的比方:
-
靠約定相同的波特率來保持收發同步,就像是兩個人約定著互相數自己的脈搏來計時,每跳動一次就接收一比特的數據,極其不靠譜。
-
而帶有時鐘信號線的通信系統,就如同在兩個人面前放了一塊表,都按照秒針的轉動來同步自己的收發,有了統一的參考,通信過程就變得靠譜多了。
我們后面的章節中即將提到的I2C總線協議,以及SPI總線協議,都是以時鐘線來保證信息的收發同步的。但是我們本節的主角,最基本的串口通信,是不具備這一特性的。筆者在這里稍作科普,方便大家對后面的通信協議的理解。接下來言歸正傳,繼續講解串口通信。
串口通信協議的數據幀¶
在串口通信中,最基本的一幀數據,至少包含了 起始位+數據位+停止位
,就如同上圖紅線框中所示。
起始位¶
起始位(Start)是必須存在的,他必須是一個bit位的低電平,即邏輯0,標志著數據傳輸即將開始。
數據位¶
緊隨其后的便是數據位(Data Bits)。數據位包含了通信中的真正有效的信息,自然也是必須存在的。通常我們在配置串口選項時可選的值為 6
, 7
, 8
, 9
,默認8
,這些數字標識了數據位的位數: 數據位的位數由通信雙方共同約定,一般可以是6位、7位或8位,比如標準的ASCII碼是0~127(7位),擴展的ASCII碼是0~255(8位),因此通信的內容如果都是ASCII碼的話,8位的2進制數構成的編碼范圍0-255即可表示完整的ASCII碼字符集。
奇偶校驗位¶
校驗位(Parity)可有可無。如果在通信協議的配置中,規定沒有校驗位,則數據位后直接跟隨停止位。
如果設置了校驗位,則有奇偶兩種校驗方式:
-
奇校驗,校驗位為邏輯1。整個數據幀中邏輯1的個數為奇數,包括奇校驗位本身的邏輯1,則滿足校驗。
-
偶校驗,校驗位為邏輯0。整個數據幀中邏輯1的個數為偶數,則滿足校驗。
相信你已經猜到了奇偶校驗位是干什么的了。沒錯,就是靠數一數每一幀的數據里邏輯1的個數是偶數還是奇數,來簡單的判斷數據在發送過程中是否有出錯。是的,這種方式乍看起來十分靠譜,因為傳輸過程中只要有一位出錯,都能夠檢測出來。除非。。。校驗位出錯了!
停止位¶
停止位(Stop Bit)是一幀數據的結束標志,停止位將信號線置位高電平??梢允?bit、1.5bit、2bit的高電平??赡艽蠹視X得很奇怪,怎么會有1.5位~沒錯,確實有的。一般我們常選1bit,這樣盡可能的壓縮了一幀數據的體積,提高了傳輸的速率。
說了這么多概念,你可能一臉懵逼。我們以傳送一個大寫字母A為例,直接上波形圖:
結合上面的概念,再來看這張圖,是不是一切都很明了了呢。
傳輸方向¶
等等,還沒有結束。在串口通信以及之后要講的幾種總線協議中,存在傳輸方向這一概念。
-
MSB(Most Significant Bit: 最高有效位), 指從一個字節的最高位向最低位的順序依次傳輸
-
LSB(Least Significant Bit:最低有效位),指從一個字節的最低為向最高位的順序依次傳輸
通常,我們默認都是MSB的傳輸方向。上面的字母A的波形圖,便是MSB。
如果是LSB,則A的Ascii碼的區間里,從左至右的0100 0001
的順序就需要顛倒過來,寫成1000 0010
如果你還想繼續鞏固對串口通信的理解,可以在B站上觀看串口通信科普視頻.
最簡單的UART串口接線¶
如下圖所示:
RX
代表信息接收端,TX
代表信息發送端。
硬件資源¶
NodeMCU-32S開發板中有三組支持串口的GPIO:
第一組是 TX0 和 RX0,這一組串口資源被REPL所占用,所以無法被用戶所使用。
組號 | RX | TX |
---|---|---|
0 | GPIO3 | GPIO1 |
1 | GPIO9 | GPIO10 |
2 | GPIO16 | GPIO17 |
不同于其他MicroPython開發板,ESP32還可以自定義GPIO作為UART,只要該GPIO滿足以下關系:
作為TX的GPIO能夠進行輸出
作為RX的GPIO能夠作為輸入
顯然,幾乎所有符合條件的GPIO都可以作為串口的輸入 RX,
除了34,35,36,39這四個GPIO只能作為輸入外,其余所有的GPIO理論上都可以作為輸出 TX
UART的API文檔¶
UART構造器¶
導入UART
模塊
from machine import UART
UART
對象的構造器函數:
UART(id, baudrate, databits, parity, rx, tx, stopbit, timeout)
參數 | 描述 |
---|---|
id |
串口編號,可用的UART資源只有兩個, id有效取值為 1 ,2 |
bandrate |
波特率,常用波特率為:9600 115200 , 默認為9600 |
databits |
數據位,是通信中的真正有效信息的每一個字節單元所包含的比特位數??蛇x的值為 6 , 7 , 8 , 9 ,默認8 。 數據位的位數由通信雙方共同約定,一般可以是6位、7位或8位,比如標準的ASCII碼是0~127(7位),擴展的ASCII碼是0~255(8位) |
parity |
基礎校驗方式 ,None 不進行校驗,0 偶校驗 1 奇校驗 |
rx |
接收口的GPIO編號 |
tx |
發送口的GPIO編號 |
stop bit |
停止位個數, 有效取值為1 ,2 , 默認值為1 |
timeout |
超時時間,取值范圍: 0 < timeout ≤ 2147483647 |
使用ID直接構造¶
上面我們說過,UART的id只能取0,1,2
的,我們可以通過id來直接構造這三組串口:
>>> from machine import UART >>> a = UART(0) Traceback (most recent call last): File "<stdin>", line 1, in <module> ValueError: UART(0) is disabled (dedicated to REPL) >>> a = UART(3) Traceback (most recent call last): File "<stdin>", line 1, in <module> ValueError: UART(3) does not exist
以上的代碼證明,我們確實無法使用第0組UART資源,他被REPL所占用,以及我們無法給串口id賦予大于2的id
構造第一組串口資源:
>>> b = UART(1) >>> b UART(1, baudrate=115201, bits=8, parity=None, stop=1, tx=10, rx=9, rts=-1, cts=-1, timeout=0, timeout_char=1) >>>
構造第二組串口資源:
>>> c = UART(2) >>> c UART(2, baudrate=115201, bits=8, parity=None, stop=1, tx=17, rx=16, rts=-1, cts=-1, timeout=0, timeout_char=1) >>>
我想細心的你已經發現,我們只需要簡單的傳入id
為1或2即可初始化構造出我們的兩組串口硬件資源,tx
和rx
的GPIO
編號都打印了出來,和上文中的硬件資源表格中筆者的標注是一一對應的。
默認的波特率為115201,近似約等于115200,這個數值取決于各個芯片的精度,介于UART的協議存在一定的容錯空間,我們將默認的波特率視為115200即可
更改管腳映射的構造¶
可能有時候你的需要使用別的管腳,不希望使用默認的GPIO資源。
以13號GPIO和12號GPIO編號為例,我們修改rx
和tx
的管腳映射,來構造一個UART
對象:
from machine import UART d = UART(2, baudrate=115200, rx=13, tx=12, timeout=10)
函數¶
在接下來的示例中, 我們構造id=1
的uart
對象來列舉UART對象的函數。
uart = UART(1)
uart.read(length)¶
函數說明:從串口讀取指定長度的數據并返回,若長度未指定則讀取所有數據。
length
: 讀入的字節數
示例:
>>> uart.read(10) # 讀入10個字符
uart.readline()¶
函數說明:從串口讀取一行數據
示例:
>>> uart.readline() # 讀入一行
uart.readinto(buf)¶
函數說明:讀入并且保存到緩沖區
buf
: 緩沖區
示例:
uart.write(data)¶
函數說明:向串口寫入(發送)數據,返回data的長度
data
: 需要寫入(發送)的數據
示例:
uart.write('abc') # 向串口寫入3個字符abc
uart.any()¶
函數說明: 檢查是否有可讀的數據,返回可讀數據的長度
示例:
uart.any() # returns the number of characters waiting
ESP32串口自發自收實驗¶
接線 將開發板的 13號引腳與12號引腳用杜邦線相連接
from machine import UART from machine import Timer import select import time # 創建一個UART對象,將13管腳和12管腳相連 # 為什么不適用UART1 默認的管腳? 親測在默認的 9,10號管腳下存在發送會觸發重啟的bug uart = UART(1, rx=13, tx=12) # 創建一個Timer,使用timer的中斷來輪詢串口是否有可讀數據 timer = Timer(1) timer.init(period=50, mode=Timer.PERIODIC, callback=lambda t: read_uart(uart)) def read_uart(uart): if uart.any(): print('received: ' + uart.read().decode() + '\n') if __name__ == '__main__': try: for i in range(10): uart.write(input('send: ')) time.sleep_ms(50) except: timer.deinit()
示例輸出:
深入學習¶
上文講解了如何使用ESP32的UART資源,如何發送與接收字符串。 如果后續深入學習的話,可能還涉及到:
-
PC串口調試助手的使用
-
自定義二進制通信協議,編寫自己的上位機
-
使用
PySerial
讓PC與ESP32進行串口通信