注意:在串口助手的接收模式中有文本模式和HEX模式兩種模式,那么它們有什么區別?
??文本模式和Hex模式是兩種不同的文件編輯或瀏覽模式,不是完全相同的概念。文本模式通常是指以ASCII編碼格式表示文本文件的編輯或瀏覽模式。在文本模式下,文本文件的內容以可讀的字符形式顯示,包括字母、數字、符號等,這些字符被轉換為計算機能夠識別和處理的二進制編碼。而Hex模式則是指以十六進制編碼格式顯示文件內容的編輯或瀏覽模式。在Hex模式下,文件的內容以16進制數值的形式顯示,每個字節(byte)用兩個十六進制數表示,從0x00到0xFF,可以查看文件的二進制編碼,包括數據、指令、標志位等信息。因此,雖然文本模式和Hex模式都是用于文件編輯或瀏覽的模式,但它們的顯示和處理方式不同,用途也不同。
STM32如何才能獲取到陀螺儀、藍牙器等這些外掛模的數據呢?
??這就需要我們在這兩個設備之間,連接上一根或多根通信線,通過通信線路發送或者接收數據,完成數據交換,從而實現控制外掛模塊和讀取外掛模塊數據的目的。所以在這里,通信的目的是,將一個設備的數據傳送到另一個設備,單片機有了通信的功能,就能與眾多別的模塊互聯,極大地擴展了硬件系統。
下面我們分別對串口通訊協議的物理層及協議層進行講解。
物理層
??串口通訊的物理層有很多標準及變種,我們主要講解RS-232標準 ,RS-232標準主要規定了信號的用途、通訊接口以及信號的電平標準。
??使用RS-232標準的串口設備間常見的通訊結構見圖 串口通訊結構圖 。
??在上面的通訊方式中,兩個通訊設備的“DB9接口”之間通過串口信號線建立起連接,串口信號線中使用“RS-232標準”傳輸數據信號。 由于RS-232電平標準的信號不能直接被控制器直接識別,所以這些信號會經過一個“電平轉換芯片”轉換成控制器能識別的“TTL標準”的電平信號,才能實現通訊。
電平標準
??根據通訊使用的電平標準不同,串口通訊可分為TTL標準及RS-232標準,見表 TTL電平標準與RS232電平標準 。
??使用RS232與TTL電平校準表示同一個信號時的對比見圖 RS-232與TTL電平標準下表示同一個信號 。
??因為控制器一般使用TTL電平標準,所以常常會使用MAX3232芯片對TTL及RS-232電平的信號進行互相轉換。
RS-232信號線
??在最初的應用中,RS-232串口標準常用于計算機、路由與調制調解器(MODEN,俗稱“貓”)之間的通訊 ,在這種通訊系統中, 設備被分為數據終端設備DTE(計算機、路由)和數據通訊設備DCE(調制調解器)。我們以這種通訊模型講解它們的信號線連接方式及各個信號線的作用。
??在舊式的臺式計算機中一般會有RS-232標準的COM口(也稱DB9接口),見圖 電腦主板上的COM口及串口線.
??其中接線口以針式引出信號線的稱為公頭,以孔式引出信號線的稱為母頭。在計算機中一般引出公頭接口,而在調制調解器設備中引出的一般為母頭,使用上圖中的串口線即可把它與計算機連接起來。通訊時,串口線中傳輸的信號就是使用前面講解的RS-232標準調制的。
??在這種應用場合下,DB9接口中的公頭及母頭的各個引腳的標準信號線接法見圖 DB9標準的公頭及母頭接法 及表 DB9信號線說明 。
??上表中的是計算機端的DB9公頭標準接法,由于兩個通訊設備之間的收發信號(RXD與TXD)應交叉相連, 所以調制調解器端的DB9母頭的收發信號接法一般與公頭的相反,兩個設備之間連接時,只要使用“直通型”的串口線連接起來即可, 見圖 計算機與調制調解器的信號線連接 。
??串口線中的RTS、CTS、DSR、DTR及DCD信號,使用邏輯 1表示信號有效,邏輯0表示信號無效。 例如,當計算機端控制DTR信號線表示為邏輯1時,它是為了告知遠端的調制調解器,本機已準備好接收數據,0則表示還沒準備就緒。
??在目前的其它工業控制使用的串口通訊中,一般只使用RXD、TXD以及GND三條信號線, 直接傳輸數據信號,而RTS、CTS、DSR、DTR及DCD信號都被裁剪掉了。
協議層
??串口通訊的數據包由發送設備通過自身的TXD接口傳輸到接收設備的RXD接口。在串口通訊的協議層中, 規定了數據包的內容,它由啟始位、主體數據、校驗位以及停止位組成,通訊雙方的數據包格式要約定一致才能正常收發數據, 其組成見圖 串口數據包的基本組成 。
串口中,每一個字節都裝載在一個數據幀里面,每個數據幀都由起始位、數據位和停止位組成.
波特率
??本章中主要講解的是串口異步通訊,異步通訊中由于沒有時鐘信號(如前面講解的DB9接口中是沒有時鐘信號的), 所以兩個通訊設備之間需要約定好波特率,即每個碼元的長度,以便對信號進行解碼, 圖 串口數據包的基本組成中用虛線分開的每一格就是代表一個碼元。常見的波特率為4800、9600、115200等。
??例如,如果每隔1秒發送一位,那么接收方也必須每隔1秒接收一位。如果接收方過早接收,則可能會重復接收某些位;如果接收方過晚接收,則可能會錯過某些位。因此,發送方和接收方必須約定好傳輸速率,這個速率參數,就是波特率。那反應到波形上,比如我們雙方規定波特率為1000bps,那就表示,1s要發1000位,每一位的時間就是1ms,發送方每隔1ms發送一位,接收方每隔1ms接收一位,這就是波特率,它決定了每隔多久發送一位。
通訊的起始和停止信號
- 起始位:
??它是標志一個數據幀的開始,固定為低電平。首先,串口的空閑狀態是高電平,也就是沒有數據傳輸的時候,然后需要傳輸的時候,必須要先發送一個起始位,這個起始位必須是低電平,來打破空閑狀態的高電平,產生一個下降沿。這個下降沿,就告訴接收設備,這一幀數據要開始了。如果沒有起始位,那當我發送8個1的時候,是不是數據線就一直都是高電平,沒有任何波動,對吧。這樣,接收方怎么知道我發送數據了呢。 - 停止位:
??同理,在一個字節數據發送完成后,必須要有一個停止位,這個停止位的作用是,用于數據幀間隔,固定為高電平。同時這個停止位,也是為下一個起始位做準備的,如果沒有停止位,那當我數據最后一位是0的時候,下次再發送新的一幀,是不是就沒法產生下降沿了,對吧。這就是起始位和停止位的作用。起始位固定為0,產生下降沿,表示傳輸開始;停止位固定為1,把引腳恢復成高電平,方便下一次的下降沿,如果沒有數據了,正好引腳也為高電平,代表空閑狀態。 - 數據位:
??這里數據位表示數據幀的有效載荷,1為高電平,0為低電平,低位先行。比如我要發送一個字節,是0x0F,那就首先把0F轉換為二進制,就是0000 1111,然后低位先行,所以數據要從低位開始發送,也就是1111 0000,像這樣,依次放在發送引腳上。所以說如果你想發0x0F這一個字節數據,那就按照波特率要求,定時翻轉引腳電平,產生一個這樣的波形就行了。
有效數據
??在數據包的起始位之后緊接著的就是要傳輸的主體數據內容,也稱為有效數據,有效數據的長度常被約定為5、6、7或8位長。
數據校驗
??最后看一下校驗位,它的用途是,用于數據驗證,是根據數據位計算得來的。這里串口,使用的是一種叫奇偶校驗的數據驗證方法,奇偶校驗可以判斷數據傳輸是不是出錯了。如果數據出錯了,可以選擇丟棄或者要求重傳,校驗可以選擇3種方式,無校驗、奇校驗和偶校驗。無校驗,就是不需要校驗位,波形就是左邊這個,起始位、數據位、停止位,總共3個部分。
??奇校驗要求有效數據和校驗位中“1”的個數為奇數,比如一個8位長的有效數據為:01101001,此時總共有4個“1”, 為達到奇校驗效果,校驗位為“1”,最后傳輸的數據將是8位的有效數據加上1位的校驗位總共9位。
??偶校驗與奇校驗要求剛好相反,要求幀數據和校驗位中“1”的個數為偶數, 比如數據幀:11001010,此時數據幀“1”的個數為4個,所以偶校驗位為“0”。
??0校驗是不管有效數據中的內容是什么,校驗位總為“0”,1校驗是校驗位總為“1”。
??當然奇偶校驗的檢出率并不是很高,比如如果有兩位數據同時出錯。奇偶特性不變,那就校驗不出來了,所以奇偶校驗只能保證一定程度上的數據校驗。如果想要更高的檢出率,可以了解一下CRC校驗,這個校驗會更加好用,當然也會更復雜。我們這個STM32內部也有CRC的外設,可以了解一下,那到這里,串口的時序我們就了解了。
說明:我們這里的數據位,有兩種表示方法,一種是把校驗位作為數據位的一部分,分為8位數據和9位數據,其中9位數據,就是8位有效載荷和1位校驗位;另一種就是把數據位和校驗位獨立開,數據位就是有效載荷,校驗位就是獨立的1位,像我這上面的描述,就是把數據位和校驗位分開描述了,在串口助手里也是分開描述,總之,無論是合在一起,還是分開描述,描述的都是同一個東西,這個應該也好理解。
串口時序
??總結一下就是,TX引腳輸出定時翻轉的高低電平,RX引腳定時讀取引腳的高低電平。每個字節的數據加上起始位、停止位、可選的校驗位,打包為數據幀,依次輸出在TX引腳,另一端RX引腳依次接收,這樣就完成了字節數據的傳遞,這就是串口通信。
STM32的USART串口
??另外我們經常還會遇到串口,叫UART,少了個S,就是通用異步收發器,一般我們串口很少使用這個同步功能,所以USART和UART使用起來,也沒有什么區別。其實這個STM32的USART同步模式,只是多了個時鐘輸出而已,它只支持時鐘輸出,不支持時鐘輸入,所以這個同步模式更多的是為了,兼容別的協議或者特殊用途而設計的,并不支持兩個USART之間進行同步通信。所以我們學習串口,主要還是異步通信。
??串行通信一般是以幀格式傳輸數據,即是一幀一幀的傳輸,每幀包含有起始信號、數據信息、停止信息, 可能還有校驗信息。USART就是對這些傳輸參數有具體規定,當然也不是只有唯一一個參數值,很多參數值都可以自定義設置,只是增強它的兼容性。
??我們之前學習了串口的協議,串口主要就是靠收發這樣的、約定好的波形來進行通信的,那這個USART外設,就是串口通信的硬件支持電路。
??這個同步模式,就是多了個時鐘CLK的輸出;硬件流控制,比如A設備的TX腳向B設備的RX腳發送數據,A設備一直在發,發的太快了,B處理不過來,如果沒有硬件流控制,那B就只能拋棄新數據或者覆蓋原數據了。如果有硬件流控制,在硬件電路上,會多出一根線,如果B沒準備好接收,就置高電平,如果準備好了,就置低電平。A接收到了B反饋的準備信號,就只會在B準備好的時候,才發數據,如果B沒準備好,那數據就不會發送出去。這就是硬件流控制,可以防止因為B處理慢而導致數據丟失的問題;之后DMA,是這個串口支持DMA進行數據轉運,可以使用DMA轉運數據,減輕CPU的負擔;最后,智能卡、IrDA、LIN,這些是其他的一些協議。因為這些協議和串口是非常的像,所以STM32就對USART加了一些小改動,就能兼容這么多協議了,不過我們一般不用,像這些協議,Up主也都沒用過。
USART框圖詳解
引腳部分:
TX: 發送數據輸出引腳。
RX: 接收數據輸入引腳。
SCLK: 發送器時鐘輸出引腳。這個引腳僅適用于同步模式。
下面這里的SWRX、IRDA_OUT/IN這些是智能卡和IrDA通信的引腳,我們不用這些協議,所以這些引腳就不用管的。
SW_RX
: 數據接收引腳,只用于單線和智能卡模式,屬于內部引腳,沒有具體外部引腳。
nRTS
: 請求以發送(Request To Send),n表示低電平有效。如果使能RTS流控制,當USART接收器準備好接收新數據時就會將nRTS變成低電平; 當接收寄存器已滿時,nRTS將被設置為高電平。該引腳只適用于硬件流控制。
nCTS
: 清除以發送(Clear To Send),n表示低電平有效。如果使能CTS流控制,發送器在發送下一幀數據之前會檢測nCTS引腳, 如果為低電平,表示可以發送數據,如果為高電平則在發送完當前數據幀之后停止發送。該引腳只適用于硬件流控制。
數據寄存器:
??USART_DR包含了已發送的數據或者接收到的數據。USART_DR實際是包含了兩個寄存器,一個專門用于發送的可寫TDR, 一個專門用于接收的可讀RDR。這兩個寄存器占用同一個地址在程序上,只表現為一個寄存器。當進行發送操作時,往USART_DR寫入數據會自動存儲在TDR內;當進行讀取操作時,向USART_DR讀取數據會自動提取RDR數據。
USART數據寄存器(USART_DR)只有低9位有效,并且第9位數據是否有效要取決于USART控制寄存器1(USART_CR1)的M位設置, 當M位為0時表示8位數據字長,當M位為1表示9位數據字長,我們一般使用8位數據字長。
??TDR和RDR都是介于系統總線和移位寄存器之間。串行通信是一個位一個位傳輸的,發送時把TDR內容轉移到發送移位寄存器, 然后把移位寄存器數據每一位發送出去,接收時把接收到的每一位順序保存在接收移位寄存器內然后才轉移到RDR。
??USART支持DMA傳輸,可以實現高速數據傳輸,具體DMA使用將在DMA章節講解。
移位寄存器:
??然后往下看,下面是兩個移位寄存器,一個用于發送,一個用于接收。發送移位寄存器的作用就是,把一個字節的數據一位一位地移出去,正好對應串口協議的波形的數據位。
這兩個寄存器是怎么工作的呢?(圖中主要講的是發送寄存器)
注意一下,當TXE標志位置1時,數據其實還沒有發送出去,只要數據從TDR轉移到發送移位寄存器了,TXE就會置1,我們就可以寫入新的數據了。【就是發送數據寄存器里一直有數據,而發送移位寄存器里的數據一旦移位完成,那么發送數據寄存器里的數據就會立刻傳輸進入發送移位寄存器里再次傳輸】
??看一下接收端這里,也是類似的。數據從RX引腳通向接收移位寄存器,在接收器控制的驅動下,一位一位地讀取RX電平,先放在最高位,然后向右移,移位8次之后,就能接收一個字節了。同樣,因為串口協議規定是低位先行,所以接收移位寄存器是從高位往低位這個方向移動的。之后,當一個字節移位完成之后,這一個字節的數據就會整體地,一下子轉移到接收數據寄存器RDR里來,在轉移的過程中,也會置一個標志位叫RXNE (RXNot Empty),接收數據寄存器非空,當我們檢測到RXNE置1之后,就可以把數據讀走了。同樣,這里也是兩個寄存器進行緩存,當數據從移位寄存器轉移到RDR時,就可以直接移位接收下一幀數據了。
??這就是USART外設整個的工作流程,其實講到這里,這個外設的主要功能就差不多了。大體上,就是數據寄存器和移位寄存器,發送移位寄存器往TX引腳移位,接收移位寄存器從RX引腳移位。當然發送還需要加上幀頭幀尾,接收還需要剔除幀頭幀尾,這些操作,它內部有電路會自動執行。我們知道有硬件幫我們做了這些工作就行了
接著我們繼續看一下下面的控制部分和一些其他的增強功能
硬件流控:
??下面這里是發送器控制,它就是用來控制發送移位寄存器的工作的;接收器控制,用來控制接收移位寄存器的工作;然后左邊這里,有一個硬件數據流控,也就是硬件流控制,簡稱流控。
??這里流控有兩個引腳,一個是nRTS,一個是nCTS。nRTS(Request To Send)是請求發送,是輸出腳,也就是告訴別人,我當前能不能接收;nCTS (Clear To Send)是清除發送,是輸入腳,也就是用于接收別人nRTS的信號的。
這里前面加個n意思是低電平有效,那這兩個腳上怎么玩的呢?
??首先,我們需要找到一個支持流控的串口,并將它的TX連接到我們的RX。同時,我們的RTS需要輸出一個接收反饋信號,并將其連接到對方的CTS。當我們可以接收數據時,RTS會置為低電平,請求對方發送。對方的CTS接收到信號后,就可以繼續發送數據。如果處理不過來,比如接收數據寄存器未及時讀取,導致新數據無法接收,此時RTS會置為高電平,對方的CTS接收到信號后,就會暫停發送,直到接收數據寄存器被讀取,RTS重新置為低電平,數據才會繼續發送。
??當我們的TX向對方發送數據時,對方的RTS會連接到我們的CTS,用于判斷對方是否可以接收數據。TX和CTS是一對對應的信號,RX和RTS也是一對對應的信號。此外,CTS和RTS之間也需要交叉連接,這就是流控的工作模式。然而,我們一般不使用流控,因此只需要了解一下即可。(少用原因應該是多消耗兩根通信線)
SCLK控制:
??接著繼續看右邊這個模塊,這部分電路用于產生同步的時鐘信號,它是配合發送移位寄存器輸出的,發送寄存器每移位一次,同步時鐘電平就跳變一個周期。時鐘告訴對方,我移出去一位數據,你看要不要讓我這個時鐘信號來指導你接收一下?當然這個時鐘只支持輸出,不支持輸入,所以兩個USART之間,不能實現同步的串口通信。
那這個時鐘信號有什么用呢?
兼容別的協議。比如串口加上時鐘之后,就跟SPI協議特別像,所以有了時鐘輸出的串口,就可以兼容SPI。另外這個時鐘也可以做自適應波特率,比如接收設備不確定發送設備給的什么波特率,然后再計算得到波特率,不過這就需要另外寫程序來實現這個功能了。這個時鐘功能,我們一般不用,所以也是了解一下就行
喚醒單元:
??這部分的作用是實現串口掛載多設備。我們之前說,串口一般是點對點的通信(只支持兩個設備互相通信)。而多設備,在一條總線上,可以接多個從設備,每個設備分配一個地址,我想跟某個設備通信,就先進行尋址,確定通信對象。那回到這里,這個喚醒單元就可以用來實現多設備的功能,在這里可以給串口分配一個地址,當你發送指定地址時,此設備喚醒開始工作,當你發送別的設備地址時,別的設備就喚醒工作,這個設備沒收到地址,就會保持沉默。這樣就可以實現多設備的串口通信了,這部分功能我們一般不用。
中斷輸出控制:
??中斷申請位,就是狀態寄存器這里的各種標志位,狀態寄存器這里,有兩個標志位比較重要,一個是TXE發送寄存器空,另一個是RXNE接收寄存器非空,這兩個是判斷發送狀態和接收狀態的必要標志位,剩下的標志位,了解一下就行。中斷輸出控制這里,就是配置中斷是不是能通向NVIC,這個應該好理解
波特率發生器部分:
??波特率發生器其實就是分頻器,APB時鐘進行分頻,得到發送和接收移位的時鐘。看一下,這里時鐘輸入是fPCLKx(x=1或2),(USART1掛載在APB2,所以就是PCLK2的時鐘,一般是72M;其他的USART都掛載在APB1,所以是PCLK1的時鐘,一般是36M)之后這個時鐘進行一個分頻,除一個USARTDIV的分頻系數,并且分為了整數部分和小數部分,因為有些波特率,用72M除一個整數的話,可能除不盡,會有誤差。所以這里分頻系數是支持小數點后4位的,分頻就更加精準,之后分頻完之后,還要再除個16,得到發送器時鐘和接收器時鐘,通向控制部分。然后右邊這里,如果TE (TX Enable)為1,就是發送器使能了,發送部分的波特率就有效;如果RE(RX Enable)為1,就是接收器使能了,接收部分的波特率就有效。
然后剩下還有一些寄存器的指示
比如各個CR控制寄存器的哪一位控制哪一部分電路,SR狀態寄存器都有哪些標志位,這些可以自己看看手冊里的寄存器描述,那里的描述比這里清晰很多
引腳定義表,這里復用功能這一欄,就給出了每個USART它的各個引腳都是復用在了哪個GPIO上的。
這些引腳都必須按照引腳定義里的規定來,或者看一下重映射這里,有沒有重映射,這里有USART1的重映射,所以有機會換一次口,剩下引腳,就沒有機會作為USART1的接口了。
USART基本結構
那到這里,USART的基本結構就講完了。
幾個小細節
數據幀:
這個圖,是在程序中配置8位字長和9位字長的波形對比。這里的字長,就是我們前面說的數據位長度。他這里的字長,是包含校驗位的,是這種描述方式。
總的來說,這里有4種選擇,9位字長,有校驗或無校驗;8位字長,有校驗或無校驗。但我們最好選擇9位字長 有校驗,或8位字長 無校驗,這兩種,這樣每一幀的有效載荷都是1字節,這樣才舒服。
配置停止位:
那最后這些時鐘什么的,和上面也都是類似的
接下來我們繼續來看這個數據幀,看一下不同停止位的波形變化。STM32的串口,可以配置停止位長度為0.5、1、1.5、2,這四種。
??這四種參數的區別,就是停止位的時長不一樣。第一個是1個停止位,這時停止位的時長就和數據位的一位,時長一樣;然后是1.5個停止位,這時的停止位就是數據位一位,時長的1.5倍;2個停止位,那停止位時長就是2倍;0.5個停止位,時長就是0.5倍。這個也好理解,就是控制停止位時長的,一般選擇1位停止位就行了,其他的參數不太常用。這個是停止位。
起始位偵測和數據采樣:
??那之后,我們繼續來看一些細節問題,這兩個圖展示的是USART電路輸入數據的一些策略。對于串口來說,根據我們前面的介紹,可以想到,串口的輸出TX應該是比輸入RX簡單很多,輸出你就定時翻轉TX引腳高低電平就行了。但是輸入,就復雜一些。你不僅要保證,輸入的采樣頻率和波特率一致,還要保證每次輸入采樣的位置,【要正好處于每一位的正中間,只有在每一位的正中間采樣,這樣高低電平讀進來,才是最可靠的,如果你采樣點過于靠前或靠后,那有可能高低電平還正在翻轉,電平還不穩定,或者稍有誤差,數據就采樣錯了】。另外,輸入最好還要對噪聲有一定的判斷能力,如果是噪聲,最好能置個標志位提醒我一下,這些就是輸入數據所面臨的問題。
那我們來看一下STM32是如何來設計輸入電路的呢?
??第一個圖展示了USART的起始位偵測。當輸入電路偵測到數據幀的起始位后,將以波特率的頻率連續采樣一幀數據。同時,從起始位開始,采樣位置要對齊到位的正中間。只要第一位對齊了,后面就都是對齊的。
??為了實現這些功能,輸入電路對采樣時鐘進行了細分,以波特率的16倍頻率進行采樣。在一位的時間里,可以進行16次采樣。比如最開始時,空閑狀態為高電平,采樣一直是1。在某個位置突然采到0,說明兩次采樣之間出現了下降沿,如果沒有噪聲,那之后就應該是起始位了。在起始位,會進行連續16次采樣,沒有噪聲的話,這16次采樣肯定都是0。但是實際電路還是會存在一些噪聲,所以這里即使出現下降沿了,后續也要再采樣幾次以防萬一。
??根據手冊描述,接收電路在下降沿之后的第3次、5次、7次進行一批采樣,在第8次、9次、10次再進行一批采樣。這兩批采樣都要求每3位里面至少應有2個0。如果沒有噪聲,那肯定全是0,滿足情況;如果有一些輕微的噪聲導致3位里面只有兩個0,另一個是1,那也算是檢測到了起始位(但是在狀態寄存器里會置一個NE(Noise Error),提醒你數據收到了但是有噪聲,你悠著點用);如果3位里面只有1個0,那就不算檢測到了起始位,可能前面那個下降沿是噪聲導致的,這時電路就忽略前面的數據重新開始捕捉下降沿。
??這就是STM32的串口在接收過程中對噪聲的處理。如果通過了這個起始位偵測那接收狀態就由空閑變為接收起始位同時第8、9、10次采樣的位置就正好是起始位的正中間。之后接收數據位時就在第8、9、10次進行采樣這樣就能保證采樣位置在位的正中間了。這就是起始位偵測和采樣位置對齊的策略。
那緊跟著,我們就可以看這個數據采樣的流程了。
??這里,從1到16,是一個數據位的時間長度,在一個數據位,有16個采樣時鐘,由于起始位偵測已經對齊了采樣時鐘,所以,這里就直接在第8、9、10次采樣數據位。為了保證數據的可靠性,這里是連續采樣3次,沒有噪聲的理想情況下,這3次肯定全為1或者全為0,全為1,就認為收到了1,全為0,就認為收到了0;如果有噪聲,導致3次采樣不是全為1或者全為0,那它就按照2:1的規則來,2次為1,就認為收到了1,2次為0,就認為收到了0,在這種情況下,噪聲標志位NE也會置1,告訴你,我收到數據了,但是有噪聲,你悠著點用,這就是檢測噪聲的數據采樣,可見STM32對這個電路的設計考慮還是很充分的
波特率發生器:
那最后,我們再來看一下波特率發生器
為什么這里公式有個16,因為它內部還有一個16倍波特率的采樣時鐘,所以這里輸入時鐘/DV要等于16倍的波特率,最終計算波特率,自然要多除一個16了。
舉個例子,比如我要配置USART1為9600的波特率,那如何配置這個BRR寄存器呢?
我們代入公式,就是9600等于 USART1的時鐘是72M 除 16倍的DIV,解得,DIV=72M/9600/16,最終等于468.75,則二進制數是11101 0100.11v。所以最終寫到這個寄存器就是整數部分為11101 0100,前面多出來的補0,小數部分為11,后面多出來的補0。這就是根據波特率寫BRR寄存器的方法,了解一下,不過,我們用庫函數配置的話,就非常方便,需要多少波特率,直接寫就行了,庫函數會自動幫我們算。
手冊講解
USB轉串口模塊的內部電路圖
代碼實戰:串口發送&&串口發送+接受
9-1串口發送:
??下面這個是我們的USB轉串口的模塊,這里有個跳線帽,上節也說過,要插在VCC和3V3這兩個腳上,選擇通信的TTL電平為3.3V,然后通信引腳,TXD和RXD,要接在STM32的PA9和PA10口。為什么是這兩個口呢,我們看一下引腳定義表就知道USART1的TX是PA9, RX是PA10,我們計劃用USART1進行通信,所以就選這兩個腳。TX和RX交叉連接,這邊一定要注意,別接錯了。然后,兩個設備之間要把負極接在一起,進行共地,一般多個系統之間互連,都要進行共地。最后,這個串口模塊和STLINK都要插在電腦上,這樣,STM32和串口模塊都有獨立供電,所以這里通信的電源正極就不需要接了。
??當然我們第一個代碼,只有STM32發送的部分,所以,通信線只有這個發送的有用,另一根線,第一個代碼沒有用到,暫時可以不接,在我們下一個串口發送+接收的代碼,兩根通信線就都需要接了。所以我們把這兩根通信線一起都接上吧,這樣兩個代碼的接線圖是一模一樣的。
老規矩,上來先寫一個初始化函數
- 第一步,開啟時鐘,把需要用的USART和GPIO的時鐘打開
- 第二步,GPIO初始化,把TX配置成復用輸出,RX配置成輸入
- 第三步,配置USART,直接使用一個結構體,就可以把這里所有的參數都配置好了
- 第四步,如果你只需要發送的功能,就直接開啟USART,初始化就結束了。如果你需要接收的功能,可能還需要配置中斷,那就在開啟USART之前,再加上ITConfig和NVIC的代碼就行了。
那初始化完成之后,如果要發送數據,調用一個發送函數就行了;如果要接收數據,就調用接收的函數;如果要獲取發送和接收的狀態,就調用獲取標志位的函數,這就是USART外設的使用思路。
Serial.c部分:
#include "stm32f10x.h" // Device header
#include <stdio.h>
#include <stdarg.h>/*** 函 數:串口初始化* 參 數:無* 返 回 值:無*/
void Serial_Init(void)
{/*開啟時鐘*/RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); //開啟USART1的時鐘RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //開啟GPIOA的時鐘/*GPIO初始化*/GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA, &GPIO_InitStructure); //將PA9引腳初始化為復用推挽輸出/*USART初始化*/USART_InitTypeDef USART_InitStructure; //定義結構體變量USART_InitStructure.USART_BaudRate = 9600; //波特率USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //硬件流控制,不需要USART_InitStructure.USART_Mode = USART_Mode_Tx; //模式,選擇為發送模式USART_InitStructure.USART_Parity = USART_Parity_No; //奇偶校驗,不需要USART_InitStructure.USART_StopBits = USART_StopBits_1; //停止位,選擇1位USART_InitStructure.USART_WordLength = USART_WordLength_8b; //字長,選擇8位USART_Init(USART1, &USART_InitStructure); //將結構體變量交給USART_Init,配置USART1/*USART使能*/USART_Cmd(USART1, ENABLE); //使能USART1,串口開始運行
}/*** 函 數:串口發送一個字節* 參 數:Byte 要發送的一個字節* 返 回 值:無*/
void Serial_SendByte(uint8_t Byte)
{USART_SendData(USART1, Byte); //將字節數據寫入數據寄存器,寫入后USART自動生成時序波形while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); //等待發送完成/*下次寫入數據寄存器會自動清除發送完成標志位,故此循環后,無需清除標志位*/
}/*** 函 數:串口發送一個數組* 參 數:Array 要發送數組的首地址* 參 數:Length 要發送數組的長度* 返 回 值:無*/
void Serial_SendArray(uint8_t *Array, uint16_t Length)
{uint16_t i;for (i = 0; i < Length; i ++) //遍歷數組{Serial_SendByte(Array[i]); //依次調用Serial_SendByte發送每個字節數據}
}/*** 函 數:串口發送一個字符串* 參 數:String 要發送字符串的首地址* 返 回 值:無*/
void Serial_SendString(char *String)
{uint8_t i;for (i = 0; String[i] != '\0'; i ++)//遍歷字符數組(字符串),遇到字符串結束標志位后停止{Serial_SendByte(String[i]); //依次調用Serial_SendByte發送每個字節數據}
}/*** 函 數:次方函數(內部使用)* 返 回 值:返回值等于X的Y次方*/
uint32_t Serial_Pow(uint32_t X, uint32_t Y)
{uint32_t Result = 1; //設置結果初值為1while (Y --) //執行Y次{Result *= X; //將X累乘到結果}return Result;
}/*** 函 數:串口發送數字* 參 數:Number 要發送的數字,范圍:0~4294967295* 參 數:Length 要發送數字的長度,范圍:0~10* 返 回 值:無*/
void Serial_SendNumber(uint32_t Number, uint8_t Length)
{uint8_t i;for (i = 0; i < Length; i ++) //根據數字長度遍歷數字的每一位{Serial_SendByte(Number / Serial_Pow(10, Length - i - 1) % 10 + '0'); //依次調用Serial_SendByte發送每位數字}
}/*** 函 數:使用printf需要重定向的底層函數* 參 數:保持原始格式即可,無需變動* 返 回 值:保持原始格式即可,無需變動*/
int fputc(int ch, FILE *f)
{Serial_SendByte(ch); //將printf的底層重定向到自己的發送字節函數return ch;
}/*** 函 數:自己封裝的prinf函數* 參 數:format 格式化字符串* 參 數:... 可變的參數列表* 返 回 值:無*/
void Serial_Printf(char *format, ...)
{char String[100]; //定義字符數組va_list arg; //定義可變參數列表數據類型的變量argva_start(arg, format); //從format開始,接收參數列表到arg變量vsprintf(String, format, arg); //使用vsprintf打印格式化字符串和參數列表到字符數組中va_end(arg); //結束變量argSerial_SendString(String); //串口發送字符數組(字符串)
}
mian.c部分:
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Serial.h"int main(void)
{/*模塊初始化*/OLED_Init(); //OLED初始化Serial_Init(); //串口初始化/*串口基本函數*/Serial_SendByte(0x41); //串口發送一個字節數據0x41uint8_t MyArray[] = {0x42, 0x43, 0x44, 0x45}; //定義數組Serial_SendArray(MyArray, 4); //串口發送一個數組Serial_SendString("\r\nNum1="); //串口發送字符串Serial_SendNumber(111, 3); //串口發送數字/*下述3種方法可實現printf的效果*//*方法1:直接重定向printf,但printf函數只有一個,此方法不能在多處使用*/printf("\r\nNum2=%d", 222); //串口發送printf打印的格式化字符串//需要重定向fputc函數,并在工程選項里勾選Use MicroLIB/*方法2:使用sprintf打印到字符數組,再用串口發送字符數組,此方法打印到字符數組,之后想怎么處理都可以,可在多處使用*/char String[100]; //定義字符數組sprintf(String, "\r\nNum3=%d", 333);//使用sprintf,把格式化字符串打印到字符數組Serial_SendString(String); //串口發送字符數組(字符串)/*方法3:將sprintf函數封裝起來,實現專用的printf,此方法就是把方法2封裝起來,更加簡潔實用,可在多處使用*/Serial_Printf("\r\nNum4=%d", 444); //串口打印字符串,使用自己封裝的函數實現printf的效果Serial_Printf("\r\n");while (1){}
}
9-2 串口發送+接受
Serial.c部分:
#include "stm32f10x.h" // Device header
#include <stdio.h>
#include <stdarg.h>uint8_t Serial_RxData; //定義串口接收的數據變量
uint8_t Serial_RxFlag; //定義串口接收的標志位變量/*** 函 數:串口初始化* 參 數:無* 返 回 值:無*/
void Serial_Init(void)
{/*開啟時鐘*/RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); //開啟USART1的時鐘RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //開啟GPIOA的時鐘/*GPIO初始化*/GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA, &GPIO_InitStructure); //將PA9引腳初始化為復用推挽輸出GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA, &GPIO_InitStructure); //將PA10引腳初始化為上拉輸入/*USART初始化*/USART_InitTypeDef USART_InitStructure; //定義結構體變量USART_InitStructure.USART_BaudRate = 9600; //波特率USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //硬件流控制,不需要USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx; //模式,發送模式和接收模式均選擇USART_InitStructure.USART_Parity = USART_Parity_No; //奇偶校驗,不需要USART_InitStructure.USART_StopBits = USART_StopBits_1; //停止位,選擇1位USART_InitStructure.USART_WordLength = USART_WordLength_8b; //字長,選擇8位USART_Init(USART1, &USART_InitStructure); //將結構體變量交給USART_Init,配置USART1/*中斷輸出配置*/USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); //開啟串口接收數據的中斷/*NVIC中斷分組*/NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //配置NVIC為分組2/*NVIC配置*/NVIC_InitTypeDef NVIC_InitStructure; //定義結構體變量NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn; //選擇配置NVIC的USART1線NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //指定NVIC線路使能NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //指定NVIC線路的搶占優先級為1NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; //指定NVIC線路的響應優先級為1NVIC_Init(&NVIC_InitStructure); //將結構體變量交給NVIC_Init,配置NVIC外設/*USART使能*/USART_Cmd(USART1, ENABLE); //使能USART1,串口開始運行
}/*** 函 數:串口發送一個字節* 參 數:Byte 要發送的一個字節* 返 回 值:無*/
void Serial_SendByte(uint8_t Byte)
{USART_SendData(USART1, Byte); //將字節數據寫入數據寄存器,寫入后USART自動生成時序波形while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); //等待發送完成/*下次寫入數據寄存器會自動清除發送完成標志位,故此循環后,無需清除標志位*/
}/*** 函 數:串口發送一個數組* 參 數:Array 要發送數組的首地址* 參 數:Length 要發送數組的長度* 返 回 值:無*/
void Serial_SendArray(uint8_t *Array, uint16_t Length)
{uint16_t i;for (i = 0; i < Length; i ++) //遍歷數組{Serial_SendByte(Array[i]); //依次調用Serial_SendByte發送每個字節數據}
}/*** 函 數:串口發送一個字符串* 參 數:String 要發送字符串的首地址* 返 回 值:無*/
void Serial_SendString(char *String)
{uint8_t i;for (i = 0; String[i] != '\0'; i ++)//遍歷字符數組(字符串),遇到字符串結束標志位后停止{Serial_SendByte(String[i]); //依次調用Serial_SendByte發送每個字節數據}
}/*** 函 數:次方函數(內部使用)* 返 回 值:返回值等于X的Y次方*/
uint32_t Serial_Pow(uint32_t X, uint32_t Y)
{uint32_t Result = 1; //設置結果初值為1while (Y --) //執行Y次{Result *= X; //將X累乘到結果}return Result;
}/*** 函 數:串口發送數字* 參 數:Number 要發送的數字,范圍:0~4294967295* 參 數:Length 要發送數字的長度,范圍:0~10* 返 回 值:無*/
void Serial_SendNumber(uint32_t Number, uint8_t Length)
{uint8_t i;for (i = 0; i < Length; i ++) //根據數字長度遍歷數字的每一位{Serial_SendByte(Number / Serial_Pow(10, Length - i - 1) % 10 + '0'); //依次調用Serial_SendByte發送每位數字}
}/*** 函 數:使用printf需要重定向的底層函數* 參 數:保持原始格式即可,無需變動* 返 回 值:保持原始格式即可,無需變動*/
int fputc(int ch, FILE *f)
{Serial_SendByte(ch); //將printf的底層重定向到自己的發送字節函數return ch;
}/*** 函 數:自己封裝的prinf函數* 參 數:format 格式化字符串* 參 數:... 可變的參數列表* 返 回 值:無*/
void Serial_Printf(char *format, ...)
{char String[100]; //定義字符數組va_list arg; //定義可變參數列表數據類型的變量argva_start(arg, format); //從format開始,接收參數列表到arg變量vsprintf(String, format, arg); //使用vsprintf打印格式化字符串和參數列表到字符數組中va_end(arg); //結束變量argSerial_SendString(String); //串口發送字符數組(字符串)
}/*** 函 數:獲取串口接收標志位* 參 數:無* 返 回 值:串口接收標志位,范圍:0~1,接收到數據后,標志位置1,讀取后標志位自動清零*/
uint8_t Serial_GetRxFlag(void)
{if (Serial_RxFlag == 1) //如果標志位為1{Serial_RxFlag = 0;return 1; //則返回1,并自動清零標志位}return 0; //如果標志位為0,則返回0
}/*** 函 數:獲取串口接收的數據* 參 數:無* 返 回 值:接收的數據,范圍:0~255*/
uint8_t Serial_GetRxData(void)
{return Serial_RxData; //返回接收的數據變量
}/*** 函 數:USART1中斷函數* 參 數:無* 返 回 值:無* 注意事項:此函數為中斷函數,無需調用,中斷觸發后自動執行* 函數名為預留的指定名稱,可以從啟動文件復制* 請確保函數名正確,不能有任何差異,否則中斷函數將不能進入*/
void USART1_IRQHandler(void)
{if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET) //判斷是否是USART1的接收事件觸發的中斷{Serial_RxData = USART_ReceiveData(USART1); //讀取數據寄存器,存放在接收的數據變量Serial_RxFlag = 1; //置接收標志位變量為1USART_ClearITPendingBit(USART1, USART_IT_RXNE); //清除USART1的RXNE標志位//讀取數據寄存器會自動清除此標志位//如果已經讀取了數據寄存器,也可以不執行此代碼}
}
main.c部分:
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Serial.h"uint8_t RxData; //定義用于接收串口數據的變量int main(void)
{/*模塊初始化*/OLED_Init(); //OLED初始化/*顯示靜態字符串*/OLED_ShowString(1, 1, "RxData:");/*串口初始化*/Serial_Init(); //串口初始化while (1){if (Serial_GetRxFlag() == 1) //檢查串口接收數據的標志位{RxData = Serial_GetRxData(); //獲取串口接收的數據Serial_SendByte(RxData); //串口將收到的數據回傳回去,用于測試OLED_ShowHexNum(1, 8, RxData, 2); //顯示串口接收的數據}}
}
USART串口數據包
先來看兩張圖,是關于我規定的數據包格式,一種是HEX數據包,一種是文本數據包,之后兩個圖,展示的就是接收數據包的思路。
接著我們來研究幾個問題:
- 第一個問題:包頭包尾和數據載荷重復的問題,這里定義FF為包頭,FE為包尾,如果我傳輸的數據本身就是FF和FE怎么辦呢?那這個問題確實存在,如果數據和包頭包尾重復,可能會引起誤判。對應這個問題我們有如下幾種解決方法:第一種,限制載荷數據的范圍。如果可以的話,我們可以在發送的時候,對數據進行限幅,比如XYZ,3個數據,變化范圍都可以是0~100 那就好辦了,我們可以在載荷中只發送0-100的數據,這樣就不會和包頭包尾重復了;第二種,如果無法避免載荷數據和包頭包尾重復,那我們就盡量使用固定長度的數據包。這樣由于載荷數據是固定的,只要我們通過包頭包尾對齊了數據,我們就可以嚴格知道,哪個數據應該是包頭包尾,哪個數據應該是載荷數據。在接收載荷數據的時候,我們并不會判斷它是否是包頭包尾,而在接收包頭包尾的時候,我們會判斷它是不是確實是包頭包尾,用于數據對齊。這樣,在經過幾個數據包的對齊之后,剩下的數據包應該就不會出現問題了;第三種,增加包頭包尾的數量,并且盡量讓它呈現出載荷數據出現不了的狀態。比如我們使用FF、FE作為包頭,FD、FC作為包尾,這樣也可以避免載荷數據和包頭包尾重復的情況發生
- 第二個問題:這個包頭包尾并不是全部都需要的,比如我們可以只要一個包頭,把包尾刪掉,這樣數據包的格式就是,一個包頭FF,加4個數據,這樣也是可以的。當檢測到FF,開始接收,收夠4個字節后,置標志位,一個數據包接收完成,這樣也可以。不過這樣的話,載荷和包頭重復的問題會更嚴重一些,比如最嚴重的情況下,我載荷全是FF,包頭也是FF,那你肯定不知道哪個是包頭了,而加上了FE作為包尾,無論數據怎么變化,都是可以分辨出包頭包尾的。
- 第三個問題:固定包長和可變包長的選擇問題,對應HEX數據包來說,如果你的載荷會出現和包頭包尾重復的情況,那就最好選擇固定包長,這樣可以避免接收錯誤,如果你又會重復,又選擇可變包長那數據很容易就亂套了;如果載荷不會和包頭包尾重復,那可以選擇可變包長,數據長度,像這樣,4位、3位、等等,1位、10位,來回任意變,肯定都沒問題。因為包頭包尾是唯一的,只要出現包頭,就開始數據包,只要出現包尾,就結束數據包,這樣就非常靈活了,這就是固定包長和可變包長選擇的問題。
- 最后一個問題:各種數據轉換為字節流的問題。這里數據包都是一個字節一個字節組成的,如果你想發送16位的整型數據、32位的整型數據,float、double,甚至是結構體,其實都沒問題,因為它們內部其實都是由一個字節一個字節組成的,只需要用一個uint8_t的指針指向它,把它們當做一個字節數組發送就行了。
好,有關HEX數據包定義的內容,就講這么多,接下來看一下文本數據包。
??文本數據包和HEX數據包分別對應了文本模式和HEX模式。在HEX數據包中,數據以原始字節形式呈現。而在文本數據包中,每個字節經過了一層編碼和譯碼,最終以文本格式呈現。實際上,每個文本字符背后都有一個字節的HEX數據。
??綜上所述,我們需要根據實際場景來選擇和設計數據包格式。在需要直接傳輸和簡單解析原始數據的情況下,HEX數據包是更好的選擇。而在需要輸入指令進行人機交互的場合,文本數據包則更為適用。
??好,數據包格式的定義講完了,接下來我們就來學一下數據包的收發流程。
??首先,發送數據包的過程相對簡單。在發送HEX數據包時,可以通過定義一個數組,填充數據,然后使用之前我們寫過的SendArray函數發送即可。在發送文本數據包時,可以通過寫一個字符串,然后調用SendString函數發送。因此,發送數據包的過程是可控的,我們可以根據需要發送任何類型的數據包。相比之下,接收數據包的過程較為復雜。
??那接下來,接收一個數據包,這就比較復雜了,我們來學習一下,我這里演示了固定包長HEX數據包的接收方法,和可變包長文本數據包的接收方法,其他的數據包也都可以套用這個形式,等會兒我們寫程序就會根據這里面的流程來。
??我們先看一下如何來接收這個固定包長的HEX數據包。要接收固定包長的HEX數據包,我們需要設計一個狀態機來處理。根據之前的代碼,我們知道每當收到一個字節,程序會進入中斷。在中斷函數里,我們可以獲取這個字節,但獲取后需要退出中斷。因此,每個收到的數據都是獨立的過程,而數據包則具有前后關聯性,包括包頭、數據和包尾。為了處理這三種狀態,我們需要設計一個能夠記住不同狀態的機制,并在不同狀態下執行不同的操作,同時進行狀態合理轉移。這種程序設計思維就是“狀態機”。
??這就是使用狀態機接收數據包的思路。這個狀態機其實是一種很廣泛的編程思路,在很多地方都可以用到,使用的基本步驟是,先根據項目要求定義狀態,畫幾個圈,然后考慮好各個狀態在什么情況下會進行轉移,如何轉移,畫好線和轉移條件,最后根據這個圖來進行編程,這樣思維就會非常清晰了。
??那接下來繼續,我們來看一下這個可變包長、文本數據包的接收流程。
??好,到這里,我們這個數據包的,定義、分類、優缺點和注意事項,就講完了,接下來,我們就來寫程序,驗證一下剛才所學的內容吧。
代碼實戰:串口收發HEX數據包&&串口收發文本數據包
9-3 串口收發HEX數據包
Serial.c部分:
#include "stm32f10x.h" // Device header
#include <stdio.h>
#include <stdarg.h>uint8_t Serial_TxPacket[4]; //定義發送數據包數組,數據包格式:FF 01 02 03 04 FE
uint8_t Serial_RxPacket[4]; //定義接收數據包數組
uint8_t Serial_RxFlag; //定義接收數據包標志位/*** 函 數:串口初始化* 參 數:無* 返 回 值:無*/
void Serial_Init(void)
{/*開啟時鐘*/RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); //開啟USART1的時鐘RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //開啟GPIOA的時鐘/*GPIO初始化*/GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA, &GPIO_InitStructure); //將PA9引腳初始化為復用推挽輸出GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA, &GPIO_InitStructure); //將PA10引腳初始化為上拉輸入/*USART初始化*/USART_InitTypeDef USART_InitStructure; //定義結構體變量USART_InitStructure.USART_BaudRate = 9600; //波特率USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //硬件流控制,不需要USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx; //模式,發送模式和接收模式均選擇USART_InitStructure.USART_Parity = USART_Parity_No; //奇偶校驗,不需要USART_InitStructure.USART_StopBits = USART_StopBits_1; //停止位,選擇1位USART_InitStructure.USART_WordLength = USART_WordLength_8b; //字長,選擇8位USART_Init(USART1, &USART_InitStructure); //將結構體變量交給USART_Init,配置USART1/*中斷輸出配置*/USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); //開啟串口接收數據的中斷/*NVIC中斷分組*/NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //配置NVIC為分組2/*NVIC配置*/NVIC_InitTypeDef NVIC_InitStructure; //定義結構體變量NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn; //選擇配置NVIC的USART1線NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //指定NVIC線路使能NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //指定NVIC線路的搶占優先級為1NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; //指定NVIC線路的響應優先級為1NVIC_Init(&NVIC_InitStructure); //將結構體變量交給NVIC_Init,配置NVIC外設/*USART使能*/USART_Cmd(USART1, ENABLE); //使能USART1,串口開始運行
}/*** 函 數:串口發送一個字節* 參 數:Byte 要發送的一個字節* 返 回 值:無*/
void Serial_SendByte(uint8_t Byte)
{USART_SendData(USART1, Byte); //將字節數據寫入數據寄存器,寫入后USART自動生成時序波形while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); //等待發送完成/*下次寫入數據寄存器會自動清除發送完成標志位,故此循環后,無需清除標志位*/
}/*** 函 數:串口發送一個數組* 參 數:Array 要發送數組的首地址* 參 數:Length 要發送數組的長度* 返 回 值:無*/
void Serial_SendArray(uint8_t *Array, uint16_t Length)
{uint16_t i;for (i = 0; i < Length; i ++) //遍歷數組{Serial_SendByte(Array[i]); //依次調用Serial_SendByte發送每個字節數據}
}/*** 函 數:串口發送一個字符串* 參 數:String 要發送字符串的首地址* 返 回 值:無*/
void Serial_SendString(char *String)
{uint8_t i;for (i = 0; String[i] != '\0'; i ++)//遍歷字符數組(字符串),遇到字符串結束標志位后停止{Serial_SendByte(String[i]); //依次調用Serial_SendByte發送每個字節數據}
}/*** 函 數:次方函數(內部使用)* 返 回 值:返回值等于X的Y次方*/
uint32_t Serial_Pow(uint32_t X, uint32_t Y)
{uint32_t Result = 1; //設置結果初值為1while (Y --) //執行Y次{Result *= X; //將X累乘到結果}return Result;
}/*** 函 數:串口發送數字* 參 數:Number 要發送的數字,范圍:0~4294967295* 參 數:Length 要發送數字的長度,范圍:0~10* 返 回 值:無*/
void Serial_SendNumber(uint32_t Number, uint8_t Length)
{uint8_t i;for (i = 0; i < Length; i ++) //根據數字長度遍歷數字的每一位{Serial_SendByte(Number / Serial_Pow(10, Length - i - 1) % 10 + '0'); //依次調用Serial_SendByte發送每位數字}
}/*** 函 數:使用printf需要重定向的底層函數* 參 數:保持原始格式即可,無需變動* 返 回 值:保持原始格式即可,無需變動*/
int fputc(int ch, FILE *f)
{Serial_SendByte(ch); //將printf的底層重定向到自己的發送字節函數return ch;
}/*** 函 數:自己封裝的prinf函數* 參 數:format 格式化字符串* 參 數:... 可變的參數列表* 返 回 值:無*/
void Serial_Printf(char *format, ...)
{char String[100]; //定義字符數組va_list arg; //定義可變參數列表數據類型的變量argva_start(arg, format); //從format開始,接收參數列表到arg變量vsprintf(String, format, arg); //使用vsprintf打印格式化字符串和參數列表到字符數組中va_end(arg); //結束變量argSerial_SendString(String); //串口發送字符數組(字符串)
}/*** 函 數:串口發送數據包* 參 數:無* 返 回 值:無* 說 明:調用此函數后,Serial_TxPacket數組的內容將加上包頭(FF)包尾(FE)后,作為數據包發送出去*/
void Serial_SendPacket(void)
{Serial_SendByte(0xFF);Serial_SendArray(Serial_TxPacket, 4);Serial_SendByte(0xFE);
}/*** 函 數:獲取串口接收數據包標志位* 參 數:無* 返 回 值:串口接收數據包標志位,范圍:0~1,接收到數據包后,標志位置1,讀取后標志位自動清零*/
uint8_t Serial_GetRxFlag(void)
{if (Serial_RxFlag == 1) //如果標志位為1{Serial_RxFlag = 0;return 1; //則返回1,并自動清零標志位}return 0; //如果標志位為0,則返回0
}/*** 函 數:USART1中斷函數* 參 數:無* 返 回 值:無* 注意事項:此函數為中斷函數,無需調用,中斷觸發后自動執行* 函數名為預留的指定名稱,可以從啟動文件復制* 請確保函數名正確,不能有任何差異,否則中斷函數將不能進入*/
void USART1_IRQHandler(void)
{static uint8_t RxState = 0; //定義表示當前狀態機狀態的靜態變量static uint8_t pRxPacket = 0; //定義表示當前接收數據位置的靜態變量if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET) //判斷是否是USART1的接收事件觸發的中斷{uint8_t RxData = USART_ReceiveData(USART1); //讀取數據寄存器,存放在接收的數據變量/*使用狀態機的思路,依次處理數據包的不同部分*//*當前狀態為0,接收數據包包頭*/if (RxState == 0){if (RxData == 0xFF) //如果數據確實是包頭{RxState = 1; //置下一個狀態pRxPacket = 0; //數據包的位置歸零}}/*當前狀態為1,接收數據包數據*/else if (RxState == 1){Serial_RxPacket[pRxPacket] = RxData; //將數據存入數據包數組的指定位置pRxPacket ++; //數據包的位置自增if (pRxPacket >= 4) //如果收夠4個數據{RxState = 2; //置下一個狀態}}/*當前狀態為2,接收數據包包尾*/else if (RxState == 2){if (RxData == 0xFE) //如果數據確實是包尾部{RxState = 0; //狀態歸0Serial_RxFlag = 1; //接收數據包標志位置1,成功接收一個數據包}}USART_ClearITPendingBit(USART1, USART_IT_RXNE); //清除標志位}
}
main.c部分:
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Serial.h"
#include "Key.h"uint8_t KeyNum; //定義用于接收按鍵鍵碼的變量int main(void)
{/*模塊初始化*/OLED_Init(); //OLED初始化Key_Init(); //按鍵初始化Serial_Init(); //串口初始化/*顯示靜態字符串*/OLED_ShowString(1, 1, "TxPacket");OLED_ShowString(3, 1, "RxPacket");/*設置發送數據包數組的初始值,用于測試*/Serial_TxPacket[0] = 0x01;Serial_TxPacket[1] = 0x02;Serial_TxPacket[2] = 0x03;Serial_TxPacket[3] = 0x04;while (1){KeyNum = Key_GetNum(); //獲取按鍵鍵碼if (KeyNum == 1) //按鍵1按下{Serial_TxPacket[0] ++; //測試數據自增Serial_TxPacket[1] ++;Serial_TxPacket[2] ++;Serial_TxPacket[3] ++;Serial_SendPacket(); //串口發送數據包Serial_TxPacketOLED_ShowHexNum(2, 1, Serial_TxPacket[0], 2); //顯示發送的數據包OLED_ShowHexNum(2, 4, Serial_TxPacket[1], 2);OLED_ShowHexNum(2, 7, Serial_TxPacket[2], 2);OLED_ShowHexNum(2, 10, Serial_TxPacket[3], 2);}if (Serial_GetRxFlag() == 1) //如果接收到數據包{OLED_ShowHexNum(4, 1, Serial_RxPacket[0], 2); //顯示接收的數據包OLED_ShowHexNum(4, 4, Serial_RxPacket[1], 2);OLED_ShowHexNum(4, 7, Serial_RxPacket[2], 2);OLED_ShowHexNum(4, 10, Serial_RxPacket[3], 2);}}
}
9-4 串口收發文本數據包
Serial.c部分:
#include "stm32f10x.h" // Device header
#include <stdio.h>
#include <stdarg.h>char Serial_RxPacket[100]; //定義接收數據包數組,數據包格式"@MSG\r\n"
uint8_t Serial_RxFlag; //定義接收數據包標志位/*** 函 數:串口初始化* 參 數:無* 返 回 值:無*/
void Serial_Init(void)
{/*開啟時鐘*/RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); //開啟USART1的時鐘RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //開啟GPIOA的時鐘/*GPIO初始化*/GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA, &GPIO_InitStructure); //將PA9引腳初始化為復用推挽輸出GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA, &GPIO_InitStructure); //將PA10引腳初始化為上拉輸入/*USART初始化*/USART_InitTypeDef USART_InitStructure; //定義結構體變量USART_InitStructure.USART_BaudRate = 9600; //波特率USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //硬件流控制,不需要USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx; //模式,發送模式和接收模式均選擇USART_InitStructure.USART_Parity = USART_Parity_No; //奇偶校驗,不需要USART_InitStructure.USART_StopBits = USART_StopBits_1; //停止位,選擇1位USART_InitStructure.USART_WordLength = USART_WordLength_8b; //字長,選擇8位USART_Init(USART1, &USART_InitStructure); //將結構體變量交給USART_Init,配置USART1/*中斷輸出配置*/USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); //開啟串口接收數據的中斷/*NVIC中斷分組*/NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //配置NVIC為分組2/*NVIC配置*/NVIC_InitTypeDef NVIC_InitStructure; //定義結構體變量NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn; //選擇配置NVIC的USART1線NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //指定NVIC線路使能NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //指定NVIC線路的搶占優先級為1NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; //指定NVIC線路的響應優先級為1NVIC_Init(&NVIC_InitStructure); //將結構體變量交給NVIC_Init,配置NVIC外設/*USART使能*/USART_Cmd(USART1, ENABLE); //使能USART1,串口開始運行
}/*** 函 數:串口發送一個字節* 參 數:Byte 要發送的一個字節* 返 回 值:無*/
void Serial_SendByte(uint8_t Byte)
{USART_SendData(USART1, Byte); //將字節數據寫入數據寄存器,寫入后USART自動生成時序波形while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); //等待發送完成/*下次寫入數據寄存器會自動清除發送完成標志位,故此循環后,無需清除標志位*/
}/*** 函 數:串口發送一個數組* 參 數:Array 要發送數組的首地址* 參 數:Length 要發送數組的長度* 返 回 值:無*/
void Serial_SendArray(uint8_t *Array, uint16_t Length)
{uint16_t i;for (i = 0; i < Length; i ++) //遍歷數組{Serial_SendByte(Array[i]); //依次調用Serial_SendByte發送每個字節數據}
}/*** 函 數:串口發送一個字符串* 參 數:String 要發送字符串的首地址* 返 回 值:無*/
void Serial_SendString(char *String)
{uint8_t i;for (i = 0; String[i] != '\0'; i ++)//遍歷字符數組(字符串),遇到字符串結束標志位后停止{Serial_SendByte(String[i]); //依次調用Serial_SendByte發送每個字節數據}
}/*** 函 數:次方函數(內部使用)* 返 回 值:返回值等于X的Y次方*/
uint32_t Serial_Pow(uint32_t X, uint32_t Y)
{uint32_t Result = 1; //設置結果初值為1while (Y --) //執行Y次{Result *= X; //將X累乘到結果}return Result;
}/*** 函 數:串口發送數字* 參 數:Number 要發送的數字,范圍:0~4294967295* 參 數:Length 要發送數字的長度,范圍:0~10* 返 回 值:無*/
void Serial_SendNumber(uint32_t Number, uint8_t Length)
{uint8_t i;for (i = 0; i < Length; i ++) //根據數字長度遍歷數字的每一位{Serial_SendByte(Number / Serial_Pow(10, Length - i - 1) % 10 + '0'); //依次調用Serial_SendByte發送每位數字}
}/*** 函 數:使用printf需要重定向的底層函數* 參 數:保持原始格式即可,無需變動* 返 回 值:保持原始格式即可,無需變動*/
int fputc(int ch, FILE *f)
{Serial_SendByte(ch); //將printf的底層重定向到自己的發送字節函數return ch;
}/*** 函 數:自己封裝的prinf函數* 參 數:format 格式化字符串* 參 數:... 可變的參數列表* 返 回 值:無*/
void Serial_Printf(char *format, ...)
{char String[100]; //定義字符數組va_list arg; //定義可變參數列表數據類型的變量argva_start(arg, format); //從format開始,接收參數列表到arg變量vsprintf(String, format, arg); //使用vsprintf打印格式化字符串和參數列表到字符數組中va_end(arg); //結束變量argSerial_SendString(String); //串口發送字符數組(字符串)
}/*** 函 數:USART1中斷函數* 參 數:無* 返 回 值:無* 注意事項:此函數為中斷函數,無需調用,中斷觸發后自動執行* 函數名為預留的指定名稱,可以從啟動文件復制* 請確保函數名正確,不能有任何差異,否則中斷函數將不能進入*/
void USART1_IRQHandler(void)
{static uint8_t RxState = 0; //定義表示當前狀態機狀態的靜態變量static uint8_t pRxPacket = 0; //定義表示當前接收數據位置的靜態變量if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET) //判斷是否是USART1的接收事件觸發的中斷{uint8_t RxData = USART_ReceiveData(USART1); //讀取數據寄存器,存放在接收的數據變量/*使用狀態機的思路,依次處理數據包的不同部分*//*當前狀態為0,接收數據包包頭*/if (RxState == 0){if (RxData == '@' && Serial_RxFlag == 0) //如果數據確實是包頭,并且上一個數據包已處理完畢{RxState = 1; //置下一個狀態pRxPacket = 0; //數據包的位置歸零}}/*當前狀態為1,接收數據包數據,同時判斷是否接收到了第一個包尾*/else if (RxState == 1){if (RxData == '\r') //如果收到第一個包尾{RxState = 2; //置下一個狀態}else //接收到了正常的數據{Serial_RxPacket[pRxPacket] = RxData; //將數據存入數據包數組的指定位置pRxPacket ++; //數據包的位置自增}}/*當前狀態為2,接收數據包第二個包尾*/else if (RxState == 2){if (RxData == '\n') //如果收到第二個包尾{RxState = 0; //狀態歸0Serial_RxPacket[pRxPacket] = '\0'; //將收到的字符數據包添加一個字符串結束標志Serial_RxFlag = 1; //接收數據包標志位置1,成功接收一個數據包}}USART_ClearITPendingBit(USART1, USART_IT_RXNE); //清除標志位}
}
mian.c部分:
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Serial.h"
#include "LED.h"
#include "string.h"int main(void)
{/*模塊初始化*/OLED_Init(); //OLED初始化LED_Init(); //LED初始化Serial_Init(); //串口初始化/*顯示靜態字符串*/OLED_ShowString(1, 1, "TxPacket");OLED_ShowString(3, 1, "RxPacket");while (1){if (Serial_RxFlag == 1) //如果接收到數據包{OLED_ShowString(4, 1, " ");OLED_ShowString(4, 1, Serial_RxPacket); //OLED清除指定位置,并顯示接收到的數據包/*將收到的數據包與預設的指令對比,以此決定將要執行的操作*/if (strcmp(Serial_RxPacket, "LED_ON") == 0) //如果收到LED_ON指令{LED1_ON(); //點亮LEDSerial_SendString("LED_ON_OK\r\n"); //串口回傳一個字符串LED_ON_OKOLED_ShowString(2, 1, " ");OLED_ShowString(2, 1, "LED_ON_OK"); //OLED清除指定位置,并顯示LED_ON_OK}else if (strcmp(Serial_RxPacket, "LED_OFF") == 0) //如果收到LED_OFF指令{LED1_OFF(); //熄滅LEDSerial_SendString("LED_OFF_OK\r\n"); //串口回傳一個字符串LED_OFF_OKOLED_ShowString(2, 1, " ");OLED_ShowString(2, 1, "LED_OFF_OK"); //OLED清除指定位置,并顯示LED_OFF_OK}else //上述所有條件均不滿足,即收到了未知指令{Serial_SendString("ERROR_COMMAND\r\n"); //串口回傳一個字符串ERROR_COMMANDOLED_ShowString(2, 1, " ");OLED_ShowString(2, 1, "ERROR_COMMAND"); //OLED清除指定位置,并顯示ERROR_COMMAND}Serial_RxFlag = 0; //處理完成后,需要將接收數據包標志位清零,否則將無法接收后續數據包}}
}