9 USART串口
注:筆記主要參考B站 江科大自化協(xié) 教學(xué)視頻“STM32入門教程-2023持續(xù)更新中”。
注:工程及代碼文件放在了本人的Github倉(cāng)庫(kù)。
9.1 串口通信協(xié)議
從本節(jié)開(kāi)始,將逐一學(xué)習(xí)STM32的通信接口。首先介紹以下stm32都集成了什么通信外設(shè)。
為了控制或讀取外掛模塊,stm32需要與外掛模塊進(jìn)行通信,來(lái)擴(kuò)展硬件系統(tǒng)。而這個(gè)“通信”的過(guò)程就需要遵守相應(yīng)的“通信協(xié)議”,也就是通信雙方需要按照協(xié)議規(guī)則進(jìn)行數(shù)據(jù)收發(fā)。不同外掛模塊的會(huì)采用不同的通信協(xié)議,如下表:
名稱 | 引腳 | 雙工 | 時(shí)鐘 | 電平 | 設(shè)備 |
---|---|---|---|---|---|
USART | TX/TXD、RX/RXD | 全雙工 | 異步 | 單端 | 點(diǎn)對(duì)點(diǎn) |
I2C | SCL、SDA | 半雙工 | 同步 | 單端 | 多設(shè)備 |
SPI | SCLK、MOSI、MISO、CS | 全雙工 | 同步 | 單端 | 多設(shè)備 |
CAN | CAN_H、CAN_L | 半雙工 | 異步 | 差分 | 多設(shè)備 |
USB | DP/D+、DM/D- | 半雙工 | 異步 | 差分 | 點(diǎn)對(duì)點(diǎn) |
下面介紹一下引腳的全稱:
- USART:TX(Transmit Exchange)數(shù)據(jù)發(fā)送腳、RX(Receive Exchange)數(shù)據(jù)接收腳。
- IIC:SCL(Serial Clock)時(shí)鐘線、SDA(Serial Data)數(shù)據(jù)線。
- SPI:MOSI(Master Output Slave Input)主機(jī)輸出數(shù)據(jù)腳、MISO(Master Input Slave Output)主機(jī)輸入數(shù)據(jù)腳、CS(Chip Select)片選
- USB:DP(Data Postive)差分線正、DM(Data Minus)差分線負(fù)
注:上述協(xié)議中,單端電平都需要共地。
注:使用差分信號(hào)可以抑制共模噪聲,可以極大的提高信號(hào)的抗干擾特性,所以一般差分信號(hào)的傳輸速度和傳輸距離都非常高。
本節(jié)將介紹串口。串口是一種應(yīng)用十分廣泛的通訊接口,串口成本低、容易使用、通信線路簡(jiǎn)單,可實(shí)現(xiàn)兩個(gè)設(shè)備的互相通信。單片機(jī)的串口可以使單片機(jī)與單片機(jī)、單片機(jī)與電腦、單片機(jī)與各式各樣的模塊互相通信,極大地?cái)U(kuò)展了單片機(jī)的應(yīng)用范圍,增強(qiáng)了單片機(jī)系統(tǒng)的硬件實(shí)力。
一般單片機(jī)中都會(huì)有串口的通信外設(shè)。在單片機(jī)領(lǐng)域中,相比于IIC、SPI等協(xié)議,串口是一種非常簡(jiǎn)單的通信接口,只支持點(diǎn)對(duì)點(diǎn)的通信。單片機(jī)與電腦通信,是串口的一大優(yōu)勢(shì),可以外接電腦屏幕,非常適合調(diào)試程序、打印信息……IIC或SPI協(xié)議一般都是芯片之間的通信,有片選引腳所以支持總線通信,而不會(huì)直接外接在電腦上。

- USB轉(zhuǎn)串口模塊:使用CH340芯片,可以將串口協(xié)議轉(zhuǎn)換成USB協(xié)議。使用此模塊可以實(shí)現(xiàn)單片機(jī)與電腦的通信。
- 陀螺儀模塊:左側(cè)是串口引腳,右側(cè)是IIC引腳??梢杂糜跍y(cè)量角速度、角速度等姿態(tài)參數(shù)。
- 藍(lán)牙串口模塊:上面的芯片可以和手機(jī)互聯(lián),下面四個(gè)引腳是串口引腳。此芯片可以實(shí)現(xiàn)手機(jī)遙控單片機(jī)的功能。

下面介紹一些串口引腳的注意事項(xiàng):
- TX與RX:簡(jiǎn)單雙向串口通信有兩根通信線(發(fā)送端TX和接收端RX),要交叉連接。不過(guò),若僅單向的數(shù)據(jù)傳輸,可以只接一根通信線。
- GND:一定要共地。由于TX和RX的高低電平都是相對(duì)于GND來(lái)說(shuō)的,所以GND嚴(yán)格來(lái)說(shuō)也算是通信線。
- VCC:相同的電平才能通信,如果兩設(shè)備都有單獨(dú)的供電,VCC就可以不接在一起。但如果某個(gè)模塊沒(méi)有供電,就需要連接VCC,注意供電電壓要按照模塊要求來(lái),必要時(shí)需要添加電壓轉(zhuǎn)換電路。
電平標(biāo)準(zhǔn)是數(shù)據(jù)1和數(shù)據(jù)0的表達(dá)方式,是傳輸線纜中人為規(guī)定的電壓與數(shù)據(jù)的對(duì)應(yīng)關(guān)系,串口常用的電平標(biāo)準(zhǔn)有如下三種:
- TTL電平【單片機(jī)】:+3.3V或+5V表示1,0V表示0。一般低壓小型設(shè)備,使用的都是TTL電平。傳輸范圍幾十米。
- RS232電平:-3~-15V表示1,+3~+15V表示0。一般在大型機(jī)器上使用,由于環(huán)境比較惡劣,靜電干擾比較大,所以通信電壓都很大,并且允許波動(dòng)的范圍也很大。傳輸范圍幾十米。
- RS485電平:兩線壓差+2~+6V表示1,-2~-6V表示0(差分信號(hào))??垢蓴_能力極強(qiáng),通信距離可達(dá)上千米。
注:不同電平標(biāo)準(zhǔn)之間的轉(zhuǎn)換只需要加電壓轉(zhuǎn)換芯片即可,并不需要修改相應(yīng)的軟件代碼。
上面介紹了串口協(xié)議的硬件部分(如何表示1/0),下面來(lái)介紹串口協(xié)議的軟件部分(如何使用1/0組成字節(jié)數(shù)據(jù))。

下面介紹串口的參數(shù):
- 波特率:串口通信的速率(bit/s),也就是通信雙方所約定的通信速率(異步通信)。
- 空閑狀態(tài):固定為高電平。
- 起始位:固定為低電平,標(biāo)志一個(gè)數(shù)據(jù)幀的開(kāi)始。
- 數(shù)據(jù)位:低位先行,數(shù)據(jù)幀的有效載荷,1為高電平,0為低電平。
- 校驗(yàn)位(選填):用于數(shù)據(jù)驗(yàn)證,根據(jù)數(shù)據(jù)位計(jì)算得來(lái)。
- 停止位:固定為高電平,用于表示數(shù)據(jù)幀的間隔,同時(shí)也可以使得通信線回歸到空閑狀態(tài)。可以配置停止位是1位/2位。

上圖給出了幾個(gè)例子,來(lái)展示串口通信的時(shí)序圖:
- 右側(cè)最后兩個(gè)圖展示了不同長(zhǎng)度停止位的現(xiàn)象。
注:在stm32中,根據(jù)發(fā)送數(shù)據(jù)自動(dòng)轉(zhuǎn)換發(fā)送波形、或根據(jù)波形自動(dòng)讀取數(shù)據(jù),都是由USART外設(shè)自動(dòng)完成的,無(wú)需軟件控制每一位的發(fā)送或讀取。
9.2 stm32的片上外設(shè)-USART
USART(Universal Synchronous/Asynchronous Receiver/Transmitter)通用同步/異步收發(fā)器 是STM32內(nèi)部集成的硬件外設(shè),可根據(jù)數(shù)據(jù)寄存器的一個(gè)字節(jié)數(shù)據(jù) 自動(dòng)生成數(shù)據(jù)幀時(shí)序,從TX引腳發(fā)送出去,也可 自動(dòng)接收RX引腳的數(shù)據(jù)幀時(shí)序,拼接為一個(gè)字節(jié)數(shù)據(jù),存放在數(shù)據(jù)寄存器里。USART中的“S”表示同步,只支持時(shí)鐘輸出,不支持時(shí)鐘輸入,是為了兼容別的協(xié)議或特殊用途而設(shè)計(jì)的,并不支持兩個(gè)USART之間進(jìn)行同步通信,所以這個(gè)功能幾乎不會(huì)用到,一般更常使用的是UART同步異步收發(fā)器。下面是一些參數(shù):
- 自帶波特率發(fā)生器,最高達(dá)4.5Mbits/s,常用9600/115200。
- 可配置數(shù)據(jù)位長(zhǎng)度(8/9)、停止位長(zhǎng)度(0.5/1/1.5/2)。
- 可選校驗(yàn)位:無(wú)校驗(yàn)(常用)/奇校驗(yàn)/偶校驗(yàn)。
- 支持同步模式(一般不用)、硬件流控制(指示從設(shè)備準(zhǔn)備好接收的信號(hào),一般不用)、DMA、智能卡、IrDA(手機(jī)紅外通信,但并不是紅外遙控,目前很少見(jiàn))、LIN(局域網(wǎng)的通信協(xié)議)。
- STM32F103C8T6 USART資源:USART1(APB2)、USART2(APB1)、USART3(APB1)。
引腳 | USART1 | USART2 | USART3 |
---|---|---|---|
TX | PA9/PB6 | PA2 | PB10 |
RX | PA10/PB7 | PA3 | PB11 |
CK | PA8 | PA4 | PB12 |
CTS | PA11 | PA0 | PB13 |
RTS | PA12 | PA1 | PB14 |
注:斜杠后面的引腳表示重定義 |

- 發(fā)送數(shù)據(jù)的過(guò)程:某時(shí)刻給“TDR”寫(xiě)入數(shù)據(jù)0x55,此時(shí)硬件檢測(cè)到寫(xiě)入數(shù)據(jù),就會(huì)檢查當(dāng)前“發(fā)送移位寄存器”是否 有數(shù)據(jù)正在進(jìn)行移位,若正在移位,就會(huì)等待移位完成;若沒(méi)有移位,就會(huì)將TDR中的數(shù)據(jù)立刻移動(dòng)到“發(fā)送移位寄存器”中,準(zhǔn)備發(fā)送。然后,“發(fā)送移位寄存器”就會(huì)在下面的“發(fā)送器控制”的驅(qū)動(dòng)下,向右一位一位的移位(低位先行),將數(shù)據(jù)輸出到TX發(fā)送引腳。移位完成后,新的數(shù)據(jù)會(huì)再次自動(dòng)的從TDR轉(zhuǎn)移到“發(fā)送移位寄存器”中來(lái)。有了TDR和“發(fā)送移位寄存器”的雙重緩存,可以保證連續(xù)發(fā)送數(shù)據(jù)時(shí),數(shù)據(jù)幀之間不會(huì)有空閑。
- 寫(xiě)入數(shù)據(jù)的時(shí)機(jī):當(dāng)數(shù)據(jù)從TDR移動(dòng)到“發(fā)送移位寄存器”時(shí),標(biāo)志位TXE置位(TX Empty,發(fā)送寄存器空),此時(shí)檢測(cè)到TXE置位就可以繼續(xù)寫(xiě)入下一個(gè)數(shù)據(jù),但注意此時(shí)上一個(gè)數(shù)據(jù)實(shí)際上還沒(méi)發(fā)送出去。
- 接收數(shù)據(jù)的過(guò)程:數(shù)據(jù)從RX引腳通向“接收移位寄存器”,在“接收器控制”的驅(qū)動(dòng)下,一位一位的讀取RX電平并放在最高位,整個(gè)過(guò)程不斷右移(低位先行),最終就可以接收1個(gè)字節(jié)。當(dāng)一個(gè)字節(jié)移位完成后,這一個(gè)字節(jié)的數(shù)據(jù)就會(huì)整體地一下子轉(zhuǎn)移到“接收數(shù)據(jù)寄存器RDR”中。然后就準(zhǔn)備繼續(xù)讀取下一幀數(shù)據(jù)。同樣,RDR和“接收移位寄存器”也組成了雙重緩存結(jié)構(gòu),可以保證連續(xù)讀取數(shù)據(jù)。
- 讀取數(shù)據(jù)的時(shí)機(jī):接收數(shù)據(jù)從“接收移位寄存器”轉(zhuǎn)移到RDR的過(guò)程中,標(biāo)志位RXNE置位(RX Not Empty,接收數(shù)據(jù)寄存器非空)。所以當(dāng)檢測(cè)到標(biāo)志位RXNE置位時(shí),就可以將數(shù)據(jù)讀走。
下面來(lái)看看其他的硬件電路功能:
- 發(fā)送器控制:控制“發(fā)送移位寄存器”工作。
- 接收器控制:控制“接收移位寄存器”工作。
- 硬件數(shù)據(jù)流控:也就是“硬件流控制”,簡(jiǎn)稱“流控”。如果主設(shè)備連續(xù)發(fā)送,導(dǎo)致從設(shè)備內(nèi)部無(wú)法及時(shí)處理接收數(shù)據(jù),就會(huì)出現(xiàn)數(shù)據(jù)丟棄或覆蓋的現(xiàn)象(注意不是UART沒(méi)接收到,而是從設(shè)備沒(méi)有及時(shí)將數(shù)據(jù)從RDR取走),此時(shí)“流控”就可以幫助從設(shè)備向主設(shè)備發(fā)信號(hào),指明自己還沒(méi)有準(zhǔn)備好接收數(shù)據(jù),主設(shè)備也就不會(huì)發(fā)送數(shù)據(jù)了。本教程不涉及。
- nRTS(Request To Send):輸出腳,請(qǐng)求發(fā)送。也就是告訴主設(shè)備,當(dāng)前是否已經(jīng)準(zhǔn)備好接收。前方“n”表示低電平有效。
- nCTS(Clear To Send):輸入腳,清除發(fā)送。用于接收從設(shè)備的nRTS信號(hào)。前方“n”表示低電平有效。
- SCLK:用于產(chǎn)生“同步功能”的時(shí)鐘信號(hào),配合“發(fā)送移位寄存器”輸出,用于給從設(shè)備提供時(shí)鐘。注意沒(méi)有同步時(shí)鐘輸入,所以兩個(gè)USART之間不能同步通信。
- 喚醒單元:實(shí)現(xiàn)串口掛載多設(shè)備。前面提到串口一般是點(diǎn)對(duì)點(diǎn)通信,但是這個(gè)模塊通過(guò)給串口分配地址“USART地址”,就可以決定是否喚醒USART工作,進(jìn)而實(shí)現(xiàn)了總線通信。
- 狀態(tài)寄存器SR:存儲(chǔ)著串口通信的各種標(biāo)志位,其中比較重要的有發(fā)送寄存器空TXE、接收數(shù)據(jù)寄存器非空RXNE。
- USART中斷控制:中斷輸出控制,配置中斷是否可以通向NVIC。其中斷申請(qǐng)位就是狀態(tài)寄存器SR中的各種標(biāo)志位。
- 波特率發(fā)生器部分:其實(shí)就是分頻器,APB時(shí)鐘進(jìn)行分頻,得到發(fā)送和接收移位的時(shí)鐘。
- fPCLKx(x=1,2):時(shí)鐘輸入。由于UASRT1掛載在APB2上,所以 時(shí)鐘輸入fPCLKx(x=1,2) 就是PCLK2的時(shí)鐘,一般為72MHz。而UASRT2、USART3都掛載在APB1上,所以 時(shí)鐘輸入fPCLKx(x=1,2) 就是PCLK1的時(shí)鐘,一般為36MHz。
- /UASRTDIV:時(shí)鐘分頻系數(shù)。內(nèi)部結(jié)構(gòu)也就是虛線框中的“傳統(tǒng)的波特率發(fā)生器”。
- /16:再進(jìn)行16分頻,得到最終的“發(fā)送器時(shí)鐘”、“接收器時(shí)鐘”。
注:發(fā)送時(shí)添加開(kāi)始位、停止位;接收時(shí)去除開(kāi)始位、停止位,這些工作由內(nèi)部硬件電路自動(dòng)完成。
注:更多關(guān)于控制寄存器CR、狀態(tài)寄存器SR的描述可以查閱參考手冊(cè)“25.6 USART寄存器描述”。

上圖給出USART最主要、最基本的結(jié)構(gòu):
- 波特率發(fā)生器:用于產(chǎn)生約定的通信速率。時(shí)鐘來(lái)源是PCLK2/PCLK1,經(jīng)過(guò)波特率發(fā)生器分頻后,產(chǎn)生的時(shí)鐘通向發(fā)送控制器、接收控制器。
- 發(fā)送控制器、接收控制器:用于控制發(fā)送移位、接收移位。
- GPIO:發(fā)送端配置成復(fù)用推挽輸出、接收端配置成上拉輸入。
- 標(biāo)志位:TXE置位時(shí)寫(xiě)入數(shù)據(jù)、RXNE置位時(shí)接收數(shù)據(jù)。
- 開(kāi)關(guān)控制:用于開(kāi)啟整個(gè)USART外設(shè)。
下面來(lái)看幾個(gè)細(xì)節(jié)的問(wèn)題:
細(xì)節(jié)1:數(shù)據(jù)幀

上圖給出了8位字長(zhǎng)(無(wú)校驗(yàn)位)、9位字長(zhǎng)(有校驗(yàn)位)的波形:
- 時(shí)鐘上升沿:可以看到每個(gè)數(shù)據(jù)中間都有一個(gè)時(shí)鐘上升沿,所以接收端采樣時(shí)刻就是時(shí)鐘上升沿。時(shí)鐘的極性、相位等都可以通過(guò)配置寄存器配置。
- 空閑幀、斷開(kāi)幀:用于局域網(wǎng)協(xié)議。
- 盡量選擇9位字長(zhǎng)有校驗(yàn)、8位字長(zhǎng)無(wú)校驗(yàn)。

stm32串口外設(shè)的停止位長(zhǎng)度可以配置成0.5/1/1.5/1位,共四種選擇,區(qū)別就是停止位的時(shí)長(zhǎng)不一樣。
- 一般就選擇停止位長(zhǎng)度為1位。
細(xì)節(jié)2:USART輸入數(shù)據(jù)采樣規(guī)則
串口的輸出TX只需要保持相應(yīng)時(shí)長(zhǎng)的電平即可,電路簡(jiǎn)單;但是串口輸入RX則需要判斷電平持續(xù)時(shí)間,所以電路會(huì)更加復(fù)雜,所以下面來(lái)詳細(xì)介紹stm32串口外設(shè)對(duì)于輸入數(shù)據(jù)的采樣。

注意到采樣時(shí)鐘是波特率的16倍,即會(huì)對(duì)某一位采樣16次。
- 檢測(cè)下降沿:若突然檢測(cè)到下降沿,則開(kāi)始進(jìn)行檢測(cè)。
- 檢測(cè)3、5、7位:在第3、5、7間隔采樣,采樣判斷原則為若三位全為0,則正常進(jìn)入后續(xù);若有2個(gè)0,則還是會(huì)接著檢測(cè),但噪聲標(biāo)志位NE(Noise Error)置位(告訴用戶,我這兒接收的信號(hào)有噪聲,你悠著點(diǎn)用??);若低于2個(gè)0,則認(rèn)為之前檢測(cè)到的下降沿為噪聲,忽略已經(jīng)捕獲的數(shù)據(jù),重新回到空閑狀態(tài)開(kāi)始捕捉下降沿。
- 檢測(cè)8、9、10位:連續(xù)采樣。采樣判斷原則與上述相同。

由于起始位采樣已經(jīng)對(duì)齊了數(shù)據(jù)時(shí)鐘,所以數(shù)據(jù)采樣就直接在8、9、10位采樣。
- 數(shù)據(jù)采樣的判斷原則:三個(gè)數(shù)據(jù)中,0/1哪個(gè)數(shù)據(jù)多就判斷為接收到哪個(gè)。但是如果三個(gè)數(shù)據(jù)有不一致的情況,噪聲標(biāo)志位NE(Noise Error)置位。
細(xì)節(jié)3:計(jì)算分頻系數(shù)DIV

發(fā)送器和接收器的波特率由波特率寄存器BRR里的分頻系數(shù)DIV確定:
波特率
=
f
P
C
L
K
2
/
1
16
?
D
I
V
波特率 = \frac{f_{PCLK2/1}}{16 * DIV}
波特率=16?DIVfPCLK2/1??
例如:若輸入時(shí)鐘為
f
P
C
L
K
2
/
1
=
72
M
H
z
f_{PCLK2/1}= 72MHz
fPCLK2/1?=72MHz,希望配置波特率為9600,則分頻系數(shù)為:
D
I
V
=
72
M
16
?
9600
=
468.75
DIV=\frac{72M}{16*9600}=468.75
DIV=16?960072M?=468.75,轉(zhuǎn)換成二進(jìn)制為111010100.11
,于是USART_BRR的值為0001_1101_0100_1100
(高位補(bǔ)零、低位補(bǔ)零)。
比較方便的是,上述過(guò)程都可以使用USART的外設(shè)庫(kù)函數(shù)實(shí)現(xiàn),調(diào)用時(shí)只需輸入波特率,庫(kù)函數(shù)會(huì)自動(dòng)計(jì)算出DIV,并按照格式配置好BRR寄存器。
注:十進(jìn)制轉(zhuǎn)二進(jìn)制工具為菜鳥(niǎo)工具的 “在線進(jìn)制轉(zhuǎn)換器”。
細(xì)節(jié)4:USB轉(zhuǎn)串口模塊

主要關(guān)注的是該模塊的供電情況。
- USB插座:直接插在電腦USB端口上,注意整個(gè)模塊的供電來(lái)自于USB的VCC+5V。
- CON6插針座:
- 引腳2、引腳3:用于連接到stm32上進(jìn)行串口通信。
- 引腳5【CH340_VCC】:通過(guò)跳線帽可以選擇 接入+3.3V(stm32) 或者+5V。CH340芯片的供電引腳,同時(shí)決定了TTL,所以也就是串口通信的TTL電平。神奇的是,即使不接跳線帽CH340也可以正常工作,TTL為3.3V,但是顯然接上電路以后更加穩(wěn)定。
- 通信和供電的選擇:CON6插針座選擇引腳4/6進(jìn)行通信后,剩下的引腳可以用于給從設(shè)備供電,但是剩下的這個(gè)腳顯然與TTL電平不匹配。此時(shí)需要注意 優(yōu)先保證供電電平的正確,通信TTL電平不一致問(wèn)題不大。當(dāng)然,若從設(shè)備自己有電源,那么就不存在這個(gè)問(wèn)題了。
- TXD指示燈、RXD指示燈:若相應(yīng)總線上有數(shù)據(jù)傳輸,那么指示燈就會(huì)閃爍。
細(xì)節(jié)5:數(shù)據(jù)模式
顯然計(jì)算機(jī)在通信過(guò)程中只能傳輸二進(jìn)制數(shù)據(jù),那么如何發(fā)送文本呢?就需要制定一個(gè)規(guī)則,來(lái)約定字符和接收數(shù)據(jù)的映射關(guān)系,即字符編碼表。不同的編碼格式有不同的映射,具體可以在網(wǎng)上隨便搜搜“字符編碼格式”,如知乎文章“字符常見(jiàn)的編碼方式”。
HEX模式/十六進(jìn)制模式/二進(jìn)制模式:以原始數(shù)據(jù)的形式顯示;
文本模式/字符模式:以原始數(shù)據(jù)編碼后的形式顯示。

9.3 USART收發(fā)相關(guān)實(shí)驗(yàn)
9.3.1 實(shí)驗(yàn)1:串口發(fā)送
需求:在軟件代碼中定義要發(fā)送的信息,然后通過(guò)串口發(fā)送到電腦端,使用“串口助手”小工具查看。要求依次發(fā)送單字節(jié)數(shù)據(jù)、數(shù)組、字符串、數(shù)據(jù)的每一位。
注:串口助手可以切換“文本模式”/“HEX模式”。
注:數(shù)字和字符的對(duì)應(yīng)關(guān)系可以參考ASCII碼表。

注:接線圖也可以不接OLED顯示屏。

下面是代碼展示:
- main.c
#include "stm32f10x.h" // Device header
#include "SerialPort.h"
int main(void){
uint8_t send_byte = 0x42;
uint8_t send_array[6] = {0x30,0x31,0x32,0x33,0x34,0x35};
//串口初始化
SerialPort_Init();
//發(fā)送單個(gè)字節(jié)
SerialPort_SendByte('A');//可以直接發(fā)送字符
SerialPort_SendByte(send_byte);
SerialPort_SendByte('\r');
SerialPort_SendByte('\n');
//發(fā)送數(shù)組
SerialPort_SendArray(send_array,6);
SerialPort_SendByte('\r');
SerialPort_SendByte('\n');
//發(fā)送字符串
SerialPort_SendString("Hello World!\r\n");
//發(fā)送數(shù)字的每一位
SerialPort_SendNum(65535, 5);
SerialPort_SendString("\r\n");
while(1){
// //循環(huán)發(fā)送數(shù)字
// SerialPort_SendByte(send_byte);
// OLED_ShowHexNum(1,9,send_byte,2);
// send_byte++;
// Delay_ms(1000);
};
}
- SerialPort.h
#ifndef __SERIALPORT_H
#define __SERIALPORT_H
void SerialPort_Init(void);
void SerialPort_SendByte(uint8_t send_byte);
void SerialPort_SendArray(uint8_t *send_array, uint16_t size_array);
void SerialPort_SendString(char *send_string);
void SerialPort_SendNum(uint32_t send_num, uint16_t send_len);
#endif
- SerialPort.c
#include "stm32f10x.h" // Device header
//串口初始化-USART1
void SerialPort_Init(void){
//1.開(kāi)啟RCC外設(shè)時(shí)鐘
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
//2.初始化GPIO-PA9
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);
//3.初始化USART結(jié)構(gòu)體
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;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_Init(USART1, &USART_InitStructure);
//4.配置中斷,開(kāi)啟NVIC(接收數(shù)據(jù)使用)
//5.開(kāi)啟外設(shè)
USART_Cmd(USART1, ENABLE);
}
//串口發(fā)送1字節(jié)數(shù)據(jù)
void SerialPort_SendByte(uint8_t send_byte){
//向發(fā)送數(shù)據(jù)寄存器TDR中寫(xiě)入數(shù)據(jù)
USART_SendData(USART1, send_byte);
//確認(rèn)數(shù)據(jù)被轉(zhuǎn)移到發(fā)送移位寄存器(等待標(biāo)志位TXE置位)
while(USART_GetFlagStatus(USART1, USART_FLAG_TXE)==RESET);
}
//發(fā)送一個(gè)數(shù)組
void SerialPort_SendArray(uint8_t *send_array, uint16_t size_array){
uint8_t i=0;
for(i=0;i<size_array;i++){
SerialPort_SendByte(send_array[i]);
}
}
//發(fā)送一個(gè)字符串
void SerialPort_SendString(char *send_string){
uint8_t i=0;
for(i=0; send_string[i]!='\0'; i++){
SerialPort_SendByte(send_string[i]);
}
}
//非外部調(diào)用函數(shù)-冪次函數(shù)
uint32_t SerialPort_Pow(uint32_t X, uint32_t Y){
uint32_t result = 1;
while(Y--){
result *= X;
}
return result;
}
//發(fā)送數(shù)字的每一位-先發(fā)高位
void SerialPort_SendNum(uint32_t send_num, uint16_t send_len){
uint16_t i;
for(i=0;i<send_len;i++){
SerialPort_SendByte((send_num/SerialPort_Pow(10,send_len-i-1))%10+'0');
}
}
編程感想:
- 關(guān)于接線。注意串口通信的兩個(gè)設(shè)備是交叉連接的,所以“USB轉(zhuǎn)串口模塊”的TX應(yīng)該接在stm32串口的RX(PA10)、RX應(yīng)該接在stm32串口的TX(PA9)!
- 關(guān)于
sizeof
。相信很多人也想到,在封裝發(fā)送數(shù)組的函數(shù)時(shí),為什么不直接在函數(shù)中使用sizeof
函數(shù)來(lái)計(jì)算數(shù)組的大小呢?這樣能少傳遞一個(gè)參數(shù),更簡(jiǎn)潔。但實(shí)際上,這里面的水很深,這樣操作是不能得到正確結(jié)果的,具體原理可以參考CSDN博文“數(shù)組名不等于指針”。最終結(jié)論就是,數(shù)組名在傳參過(guò)程中,會(huì)退化成一個(gè)指針,此時(shí)所表示的大小不是數(shù)組的實(shí)際大小,而是這個(gè)指針的大小,所以要想在函數(shù)中使用到數(shù)組大小,最好還是多傳遞一個(gè)參數(shù)。
9.3.2 實(shí)驗(yàn)2:移植printf
函數(shù)
需求:將C語(yǔ)言自帶函數(shù)printf
進(jìn)行封裝,默認(rèn)成將需要打印的數(shù)據(jù)發(fā)送到串口,進(jìn)而可以顯示在電腦端串口助手上。
接線圖、函數(shù)調(diào)用(非庫(kù)函數(shù)) 與上一小節(jié)實(shí)驗(yàn)“串口發(fā)送”相同。本節(jié)本人目前也不懂原理,所以下面直接給出 代碼展示:
方法一:
- 首先點(diǎn)擊“魔術(shù)棒”,在Target界面的“Code Generation”方框中勾選“USE MicroLIB”。MicroLIB是Keil為嵌入式平臺(tái)優(yōu)化的一個(gè)精簡(jiǎn)庫(kù),要使用
printf
函數(shù)就會(huì)用到這個(gè)MicroLIB精簡(jiǎn)庫(kù)。 - 對(duì)
printf
函數(shù)重定向,將printf
函數(shù)打印的東西輸出到串口。于是在 SerialPort.c 模塊中添加下列代碼:
#include <stdio.h>
//對(duì)printf函數(shù)重定向-將fputc函數(shù)原型重定向到串口
//注:ptintf函數(shù)本質(zhì)上就是循環(huán)調(diào)用fputc,將字符一個(gè)一個(gè)輸出
int fputc(int ch, FILE *f){
SerialPort_SendByte(ch);
return ch;
}
在 SerialPort.h 模塊中添加下列代碼:
#include <stdio.h>
- 于是就可以在 main.c 中調(diào)用
printf
函數(shù),將數(shù)據(jù)輸出到串口了。
//使用重定向的printf函數(shù)
printf("%d\r\n",666);
但注意這個(gè)方法直接將printf
函數(shù)重定向到了USART1,別的USART外設(shè)(USART2、USART3等)想用就沒(méi)辦法了。所以有如下的改進(jìn)方法。
方法二:
若多個(gè)串口都想使用printf
函數(shù),那么就可以使用sprintf
函數(shù)。sprintf
函數(shù)可以將格式化字符輸出到一個(gè)字符串里,然后再調(diào)用相應(yīng)的“串口發(fā)送字符串”函數(shù)發(fā)送這個(gè)字符串,整個(gè)過(guò)程不涉及重定向,于是就實(shí)現(xiàn)了所有USART外設(shè)都可以打印信息到串口了。所以下面可以直接在 main.c 中定義:
char String[100];//定義一個(gè)足夠長(zhǎng)的字符串?dāng)?shù)組
sprintf(String, "Num=%d\r\n", 666);//將格式化字符串存儲(chǔ)在String中
SerialPort_SendString(String);//串口發(fā)送字符串
方法三:
方法二的sprintf
函數(shù)很方便,但是直接在主函數(shù)中寫(xiě)比較麻煩,于是本方法就是來(lái)封裝“方法二”。具體方法如下:
- C語(yǔ)言可變參數(shù)。在串口模塊
SerialPort.c
中添加下面代碼:
#include <stdarg.h>
//對(duì)sprintf函數(shù)進(jìn)行封裝
void SerialPort_Printf(char *format,...){
char String[100];//定義輸出的字符串
va_list arg;//定義參數(shù)列表變量
va_start(arg, format);//從format位置開(kāi)始接收參數(shù)表,放在arg里面
vsprintf(String, format, arg);//sprintf只接收直接寫(xiě)的參數(shù),對(duì)于封裝格式改用vsprintf
va_end(arg);//釋放參數(shù)表
SerialPort_SendString(String);
}
//注意上述函數(shù)要在頭文件中聲明,但頭文件就不需要再添加<stdarg.h>了。
- 直接在主函數(shù)中調(diào)用:
SerialPort_Printf("Num=%d\r\n", 888);
特殊說(shuō)明:顯示漢字
使用上面幾種printf
函數(shù)時(shí),有可能會(huì)出現(xiàn)亂碼,要解決這個(gè)問(wèn)題,關(guān)鍵是要保持Keil編譯器(“扳手”圖標(biāo)Editor界面的Encoding下拉菜單)和“串口助手”的文本編碼一致。可以選擇以下兩種情況:
- 兩者都選擇UTF-8編碼。但是直接在字符串寫(xiě)漢字,編譯器有可能報(bào)錯(cuò),解決方法是點(diǎn)擊“魔術(shù)棒”–>C/C+±->最下面的Controls輸入
--no-multibyte-chars
即可。- 有些串口助手軟件可能不兼容UTF-8,所以兩者都需要選擇GB2312編碼。
注:更改編譯器文本編碼格式后,需要將文件全部關(guān)閉,重新打開(kāi),否則編碼方式不會(huì)改變。
9.3.3 實(shí)驗(yàn)3:串口發(fā)送+接收
需求:電腦端發(fā)送數(shù)據(jù),stm32接收到數(shù)據(jù)后,將數(shù)據(jù)在OLED顯示屏上顯示出來(lái)、并且回傳到電腦。
程序整體思路:
- 查詢。主函數(shù)不斷查詢RXNE標(biāo)志位,但是會(huì)占用很多的CPU資源,所以不推薦。
- 中斷。推薦方法,下面的演示也是基于此方法。
本實(shí)驗(yàn)的 接線圖 與前兩小節(jié)的實(shí)驗(yàn)均相同,下面給出 代碼調(diào)用:

下面是 代碼展示,僅給出在串口模塊中增添的函數(shù):
- main.c
#include "stm32f10x.h" // Device header
#include "OLED.h"
#include "SerialPort.h"
int main(void){
uint8_t Rx_byte = 0;//串口接收的單比特?cái)?shù)據(jù)
//設(shè)置中斷分組
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
//OLED初始化
OLED_Init();
OLED_ShowString(1,1,"Rx_byte:");
//串口初始化
SerialPort_Init();
while(1){
if(SerialPort_GetRxFlag()==1){
Rx_byte = SerialPort_GetRxData();
OLED_ShowHexNum(1,9,Rx_byte,2);
SerialPort_SendByte(Rx_byte);
}
};
}
- SerialPort.c
uint8_t SerialPort_RxData = 0;
uint8_t SerialPort_RxFlag = 0;
//獲取接收的狀態(tài)
uint8_t SerialPort_GetRxFlag(void){
if(SerialPort_RxFlag==1){
SerialPort_RxFlag = 0;
return 1;
}else{
return 0;
}
}
//獲取接收的數(shù)據(jù)
uint8_t SerialPort_GetRxData(void){
return SerialPort_RxData;
}
//USART1_RXNE中斷函數(shù)
void USART1_IRQHandler(void){
if(USART_GetITStatus(USART1, USART_IT_RXNE)==SET){
SerialPort_RxFlag = 1;
SerialPort_RxData = USART_ReceiveData(USART1);
//讀操作可以自動(dòng)清零標(biāo)志位,但加上也沒(méi)事
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
}
//不要忘了將前兩個(gè)函數(shù)在頭文件中聲明
9.4 USART串口數(shù)據(jù)包
數(shù)據(jù)包的作用是將一個(gè)個(gè)單獨(dú)的數(shù)據(jù)打包,方便進(jìn)行多字節(jié)的數(shù)據(jù)通信。因?yàn)閷?shí)際應(yīng)用中,經(jīng)常需要進(jìn)行數(shù)據(jù)打包。比如陀螺儀傳感器需要將數(shù)據(jù)發(fā)送到stm32,其中包括X軸、Y軸、Z軸三個(gè)字節(jié),循環(huán)不斷的發(fā)送;若采用一個(gè)一個(gè)進(jìn)行發(fā)送的方式,接收方就有可能分不清對(duì)應(yīng)的順序,進(jìn)而出現(xiàn)數(shù)據(jù)錯(cuò)位現(xiàn)象。此時(shí),若能將同一批數(shù)據(jù)進(jìn)行分割和打包,就可以方便接收方識(shí)別。
1. 數(shù)據(jù)包格式的定義:

若載荷數(shù)據(jù)與包頭、包尾一樣怎么辦呢?有三種解決思路:
- 限制載荷數(shù)據(jù)的范圍。使其不會(huì)與包頭、包尾重復(fù)。
- 盡量使用固定長(zhǎng)度的數(shù)據(jù)包。只要數(shù)據(jù)長(zhǎng)度固定,那么就可以通過(guò)包頭、包尾定位數(shù)據(jù)。
- 增加包頭包尾的數(shù)量,使其盡量呈現(xiàn)出載荷數(shù)據(jù)不會(huì)出現(xiàn)的狀態(tài)。
注:包尾可以去除。但是這樣會(huì)使得載荷數(shù)據(jù)和包頭重復(fù)的問(wèn)題更加嚴(yán)重。
若想發(fā)送16位整型數(shù)據(jù)、32位整型數(shù)據(jù)、float、double、結(jié)構(gòu)體等,只需使用 uint8_t
型指針 指向這些數(shù)據(jù),就可以進(jìn)行發(fā)送(將各種數(shù)據(jù)轉(zhuǎn)換成字節(jié)流)。

文本數(shù)據(jù)包中,每個(gè)數(shù)據(jù)都經(jīng)過(guò)了一層編碼和譯碼。
由于包頭包尾非常容易唯一確定,文本數(shù)據(jù)包基本不用擔(dān)心載荷數(shù)據(jù)和包頭包尾重復(fù)的問(wèn)題。
優(yōu)缺點(diǎn)比較:
- HEX數(shù)據(jù)包:
- 優(yōu)點(diǎn):傳輸最直接,解析數(shù)據(jù)非常簡(jiǎn)單,比較適合一些模塊發(fā)送最原始的數(shù)據(jù)。如使用串口通信的陀螺儀、溫濕度傳感器。
- 缺點(diǎn):靈活性不足,載荷容易和包頭包尾重復(fù)。
- 文本數(shù)據(jù)包:
- 優(yōu)點(diǎn):數(shù)據(jù)直觀易理解,非常靈活,比較適合一些輸入指令進(jìn)行人機(jī)交互的場(chǎng)合。如藍(lán)牙模塊常用的AT指令、CNC和3D打印機(jī)常用的G代碼,都是文本數(shù)據(jù)包的格式。
- 缺點(diǎn):解析效率低。
2. 數(shù)據(jù)包格式的收發(fā)流程:
數(shù)據(jù)包發(fā)送是非常簡(jiǎn)單的,直接發(fā)就完事兒了。但是接收數(shù)據(jù)包的過(guò)程比較復(fù)雜,這是就要考慮使用狀態(tài)機(jī)。


9.5 USART數(shù)據(jù)包相關(guān)實(shí)驗(yàn)
9.5.1 實(shí)驗(yàn)1:串口收發(fā)HEX數(shù)據(jù)包
需求:自定義數(shù)據(jù)包格式,使用串口完成指定格式的數(shù)據(jù)包收發(fā),并將收發(fā)結(jié)果顯示在OLED上。另外按鍵的功能是將發(fā)送的當(dāng)前存儲(chǔ)的發(fā)送數(shù)據(jù)全部加1再發(fā)送出去。
- 數(shù)據(jù)包頭:0xFF。
- 載荷數(shù)據(jù):固定數(shù)據(jù)段長(zhǎng)度為4個(gè)字節(jié)。
- 數(shù)據(jù)包尾:0xFE。


下面是代碼展示:其中將串口的模塊的數(shù)據(jù)接收部分全部刪除,重寫(xiě)。
- main.c
#include "stm32f10x.h" // Device header
#include "OLED.h"
#include "SerialPort.h"
#include "Key.h"
int main(void){
//存儲(chǔ)串口接收的HEX數(shù)據(jù)包
uint8_t *Rx_Packet = SerialPort_GetRxPacket();
//存儲(chǔ)串口發(fā)送的HEX數(shù)據(jù)包
uint8_t Tx_Packet[4] = {0x01,0x02,0x03,0x04};
//設(shè)置中斷分組
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
//OLED初始化
OLED_Init();
OLED_ShowString(1,1,"Rx_Packet:");
OLED_ShowString(3,1,"Tx_Packet:");
//串口初始化
SerialPort_Init();
//按鍵初始化
Key_Init();
while(1){
//顯示接收到的數(shù)據(jù)
if(SerialPort_GetRxFlag()==1){
OLED_ShowHexNum(2,1,*Rx_Packet,2);
OLED_ShowHexNum(2,4,*(Rx_Packet+1),2);
OLED_ShowHexNum(2,7,*(Rx_Packet+2),2);
OLED_ShowHexNum(2,10,*(Rx_Packet+3),2);
}
//檢測(cè)按鍵,發(fā)送數(shù)據(jù)包到電腦
if(Key_GetNum()==1){
Tx_Packet[0]++;
Tx_Packet[1]++;
Tx_Packet[2]++;
Tx_Packet[3]++;
SerialPort_SendPacket(Tx_Packet);
OLED_ShowHexNum(4,1,Tx_Packet[0],2);
OLED_ShowHexNum(4,4,Tx_Packet[1],2);
OLED_ShowHexNum(4,7,Tx_Packet[2],2);
OLED_ShowHexNum(4,10,Tx_Packet[3],2);
}
};
}
- SerialPort.c新增函數(shù)
uint8_t SerialPort_RxPacket[4];
uint8_t SerialPort_RxPacketFlag = 0;
//獲取接收的狀態(tài)
uint8_t SerialPort_GetRxFlag(void){
if(SerialPort_RxPacketFlag==1){
SerialPort_RxPacketFlag = 0;
return 1;
}else{
return 0;
}
}
//獲取接收的HEX數(shù)據(jù)包
uint8_t* SerialPort_GetRxPacket(void){
return SerialPort_RxPacket;
}
//發(fā)送HEX數(shù)據(jù)包
void SerialPort_SendPacket(uint8_t *send_array){
SerialPort_SendByte(0xFF);
SerialPort_SendArray(send_array, 4);
SerialPort_SendByte(0xFE);
}
//USART1_RXNE中斷函數(shù)
void USART1_IRQHandler(void){
uint8_t rec_byte;
static uint8_t rx_state;
static uint8_t rx_index;
if(USART_GetITStatus(USART1, USART_IT_RXNE)==SET){
rec_byte = USART_ReceiveData(USART1);
//利用狀態(tài)機(jī),接收HEX數(shù)據(jù)包
if(rx_state==0){
if(rec_byte==0xFF){
rx_index = 0;
rx_state = 1;
}
}else if(rx_state==1){
SerialPort_RxPacket[rx_index] = rec_byte;
rx_index++;
if(rx_index>=4){
rx_state = 2;
}
}else if(rx_state==2){
if(rec_byte==0xFE){
SerialPort_RxPacketFlag = 1;
rx_state = 0;
}
}
//讀操作可以自動(dòng)清零標(biāo)志位,但加上也沒(méi)事
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
}
//除了中斷函數(shù),其余函數(shù)還要在頭文件SerialPort.h中聲明
編程感想:
- 數(shù)據(jù)混疊。若電腦端連續(xù)發(fā)送數(shù)據(jù)包,而stm32處理不及時(shí),會(huì)導(dǎo)致數(shù)據(jù)錯(cuò)位。但是一般像傳感器模塊等的數(shù)據(jù)都具有連續(xù)性,所以就算數(shù)據(jù)錯(cuò)位也沒(méi)關(guān)系。
- 發(fā)送數(shù)據(jù)不匹配。注意發(fā)送字節(jié)數(shù)據(jù)一定要寫(xiě)成
0x11
的形式,而不是直接寫(xiě)一個(gè)11
進(jìn)行發(fā)送。- 收發(fā)數(shù)據(jù)沒(méi)反應(yīng)。注意一定要在最開(kāi)始聲明的地方賦初值,否則有可能讀不出數(shù)據(jù)。當(dāng)然,還有一種可能是串口連接不穩(wěn)定,可以重新拔插一下串口。
9.5.2 實(shí)驗(yàn)2:串口收發(fā)文本數(shù)據(jù)包
需求:使用電腦端發(fā)送指定格式的文本數(shù)據(jù)包,來(lái)控制單片機(jī)點(diǎn)亮或熄滅LED燈,單片機(jī)完成指令后再將接收的狀態(tài)回傳到電腦。
- 數(shù)據(jù)包頭:@。
- 數(shù)據(jù)包:有效指令為"@LED_ON\r\n"、“@LED_OFF\r\n”。(不定字長(zhǎng))
- 數(shù)據(jù)包尾:\r\n。


下面是代碼展示:
- main.c
#include "stm32f10x.h" // Device header
#include "OLED.h"
#include "SerialPort.h"
#include "LED.h"
#include <string.h>
int main(void){
//存儲(chǔ)串口接收的HEX數(shù)據(jù)包
char *Rx_Packet = SerialPort_GetRxPacket();
//設(shè)置中斷分組
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
//OLED初始化
OLED_Init();
OLED_ShowString(1,1,"Rx_Packet:");
OLED_ShowString(3,1,"Tx_Packet:");
//串口初始化
SerialPort_Init();
//LED初始化
LED_Init();
while(1){
//對(duì)接收到的文本進(jìn)行判斷
if(SerialPort_GetRxFlag()==1){
//OLED顯示接收到的文本
OLED_ShowString(2,1," ");
OLED_ShowString(2,1,Rx_Packet);
//根據(jù)接收的內(nèi)容執(zhí)行相應(yīng)的動(dòng)作
if(strcmp(Rx_Packet, "LED_ON")==0){
LED1_ON();
OLED_ShowString(4,1," ");
OLED_ShowString(4,1,"LED_ON_OK");
SerialPort_SendString("LED_ON_OK\r\n");
}else if(strcmp(Rx_Packet, "LED_OFF")==0){
LED1_OFF();
OLED_ShowString(4,1," ");
OLED_ShowString(4,1,"LED_OFF_OK");
SerialPort_SendString("LED_OFF_OK\r\n");
}else{
OLED_ShowString(4,1," ");
OLED_ShowString(4,1,"ERROR_COMMAND");
SerialPort_SendString("ERROR_COMMAND\r\n");
}
}
};
}
- SerialPort.c新增函數(shù)(將上一節(jié)HEX數(shù)據(jù)包部分全部刪除)
char SerialPort_RxPacket[100];
uint8_t SerialPort_RxPacketFlag = 0;
//獲取接收的狀態(tài)
uint8_t SerialPort_GetRxFlag(void){
if(SerialPort_RxPacketFlag==1){
SerialPort_RxPacketFlag = 0;
return 1;
}else{
return 0;
}
}
//獲取接收的HEX數(shù)據(jù)包
char* SerialPort_GetRxPacket(void){
return SerialPort_RxPacket;
}
//USART1_RXNE中斷函數(shù)
void USART1_IRQHandler(void){
uint8_t rec_byte;
static uint8_t rx_state;
static uint8_t rx_index;
if(USART_GetITStatus(USART1, USART_IT_RXNE)==SET){
rec_byte = USART_ReceiveData(USART1);
//利用狀態(tài)機(jī),接收HEX數(shù)據(jù)包
if(rx_state==0){
if(rec_byte== '@'){
rx_index = 0;
rx_state = 1;
}
}else if(rx_state==1){
if(rec_byte != '\r'){
SerialPort_RxPacket[rx_index] = rec_byte;
rx_index++;
}else{
rx_state = 2;
}
}else if(rx_state==2){
if(rec_byte == '\n'){
SerialPort_RxPacket[rx_index] = '\0';//字符串結(jié)束標(biāo)志符
SerialPort_RxPacketFlag = 1;
rx_state = 0;
}
}
//讀操作可以自動(dòng)清零標(biāo)志位,但加上也沒(méi)事
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
}
//除了中斷函數(shù),其他函數(shù)還要在頭文件SerialPort.h中聲明
- LED.c新增函數(shù)
/**
* @brief LED1亮
*/
void LED1_ON(void){
GPIO_ResetBits(GPIOA, GPIO_Pin_1);
}
/**
* @brief LED1滅
*/
void LED1_OFF(void){
GPIO_SetBits(GPIOA, GPIO_Pin_1);
}
//注意還要在頭文件LED.h中聲明
編程感想:
- 數(shù)據(jù)混疊。同樣的問(wèn)題,如果電腦端發(fā)送指令太快,就有可能導(dǎo)致stm32來(lái)不及處理,導(dǎo)致錯(cuò)誤。一個(gè)解決方法是指令發(fā)慢一點(diǎn);當(dāng)然另一種方法是增加標(biāo)志位,當(dāng)LED的狀態(tài)沒(méi)有改變之前,不理會(huì)接收數(shù)據(jù),這樣會(huì)破壞當(dāng)前程序的獨(dú)立性,并且原理不復(fù)雜,我就先不寫(xiě)了。
9.6 軟件使用:FlyMcu串口下載 & STLINK Utility
- FlyMcu可以通過(guò)串口給stm32下載程序。綠色軟件,無(wú)需安裝。
- STLINK Utility通過(guò)STLINK給stm32下載程序。需要安裝。
以上兩款軟件類似于51單片機(jī)的程序燒錄軟件——STC-ISP,可以通過(guò)串口給51單片機(jī)下載程序。
硬件方面,接線圖本章的第一個(gè)實(shí)驗(yàn)“串口發(fā)送”相同,這是因?yàn)樵撔酒拇谙螺d只適配了USART1,所以引腳都要連接在USART1引腳上。

下面依次介紹FlyMcu、STLINK Utility:

FlyMcu只能下載HEX文件,Keil生成HEX文件 的方法:
- 點(diǎn)擊“魔術(shù)棒”–>Output選項(xiàng)卡–>勾選“Create HEX File”。編譯過(guò)后,就可以在工程目錄的“Object文件夾”下找到該工程的HEX文件——“工程名.hex”。
使用FlyMcu下載程序:
- 配置下載串口:點(diǎn)擊菜單欄“搜索串口”,稍等一會(huì),點(diǎn)擊緊隨其后的“Port:xx”選項(xiàng),選擇對(duì)應(yīng)的串口。接著點(diǎn)擊緊隨其后的“bps:xx”選擇下載的波特率。
- 選擇程序文件。菜單欄下一行,點(diǎn)擊“…”按鈕,選擇HEX文件。
- 其他功能保持默認(rèn)。
- 調(diào)整啟動(dòng)位置為BootLoader程序。由于串口下載需要使得stm32的啟動(dòng)位置為BootLoader啟動(dòng)程序,而B(niǎo)ootLoader啟動(dòng)程序固定放在“系統(tǒng)存儲(chǔ)器”,所以BOOT引腳應(yīng)調(diào)整為 [BOOT1,BOOT0]=[0,1]。注意調(diào)整BOOT后,一定要按一下最小系統(tǒng)板上的復(fù)位鍵,調(diào)整才會(huì)生效。
![]()
- 點(diǎn)擊FlyMcu的“開(kāi)始編程”。此時(shí)程序就會(huì)被下載到主閃存中了,但由于BOOT引腳沒(méi)有變動(dòng),所以程序下載完復(fù)位后,還是會(huì)停留在BootLoader程序中。
- 調(diào)整啟動(dòng)位置為主閃存。BOOT引腳應(yīng)調(diào)整為 [BOOT1,BOOT0]=[0,0],然后按下復(fù)位鍵,就可以看到程序正常運(yùn)行了。
注:關(guān)于串口下載的原理,我覺(jué)得前面將“存儲(chǔ)器映像”時(shí)已經(jīng)說(shuō)得很清楚了,就不記錄了,UP講解的地方在“P30 [9-6] FlyMcu串口下載&STLINK Utility 中的4:56~8:11”。
每次下載程序都要拔插兩邊跳線帽,太麻煩了,有什么更簡(jiǎn)單的方法嗎?
- 方法一:設(shè)計(jì)外圍的STM32一鍵下載電路。利用“USB轉(zhuǎn)串口模塊”上CH340的 RTS#、DTR# 兩個(gè)流控輸出引腳分別控制stm32的BOOT0引腳、復(fù)位引腳(但是不使用流控功能,而僅僅只是當(dāng)作一個(gè)普通的GPIO口),加上FlyMcu的選項(xiàng),便可以利用電路自動(dòng)切換BOOT引腳的高低電平。
- 方法二:軟件設(shè)置自動(dòng)跳轉(zhuǎn)(一次性功能)。勾選FlyMcu的“編程后執(zhí)行”,去除勾選“編程到FLASH時(shí)寫(xiě)選項(xiàng)字節(jié)”。然后 [BOOT1,BOOT0]=[0,1],并按下復(fù)位鍵。然后點(diǎn)擊“開(kāi)始編程”,就可以看到程序正常執(zhí)行。
方法二評(píng)價(jià):該方法原理是下載程序后,軟件設(shè)置從主閃存(0x08000000)開(kāi)始執(zhí)行程序,但是按下復(fù)位鍵后,程序又會(huì)回到“系統(tǒng)存儲(chǔ)器”執(zhí)行BootLoader程序。所以可以不斷的使用串口調(diào)試程序,最后調(diào)試成功后再將BOOT引腳切換回來(lái)即可。也就是,只需要最開(kāi)始和最后切換跳線帽而已。
讀FLASH: 讀取主閃存中的程序數(shù)據(jù),以BIN文件格式存儲(chǔ)(BIN文件不包含地址信息,HEX文件包含地址信息)。后續(xù)可以使用STLINK Utility下載,實(shí)現(xiàn)盜取他人軟件勞動(dòng)成果??。
清除芯片: 將主程序區(qū)域全部擦除(全部變成高電平)。
讀器件信息: 讀取芯片的信息。但是FLASH容量、SRAM容量等信息可能會(huì)出錯(cuò)。
設(shè)定選項(xiàng)字節(jié)等: 可以設(shè)置選項(xiàng)字節(jié)的各項(xiàng)參數(shù)。點(diǎn)擊“STM32F1選項(xiàng)設(shè)置”,彈出以下界面:
- 讀保護(hù)字節(jié):是否允許讀出主閃存數(shù)據(jù)。注意如果“阻止讀出”,那么就無(wú)法使用Keil下載程序了。另外,在清除讀保護(hù)的同時(shí),同時(shí)也會(huì)清空主閃存的數(shù)據(jù)。
- 硬件選項(xiàng)字節(jié):有需求可以使用。
- 用戶數(shù)據(jù)字節(jié):有需求可以使用。那使用選項(xiàng)字節(jié)存儲(chǔ)數(shù)據(jù)有什么好處呢?
- 選項(xiàng)字節(jié)的數(shù)據(jù)相當(dāng)于是“世外桃源”,無(wú)論如何下載程序,選項(xiàng)字節(jié)中的數(shù)據(jù)都可以不變,可以存儲(chǔ)一些不隨程序變化的參數(shù);
- 用上位機(jī)(如FlyMcu、STLINK Utility)可以很方便的修改。
- 寫(xiě)保護(hù)字節(jié):可以對(duì)Flash的某幾頁(yè)單獨(dú)寫(xiě)保護(hù)。比如在主程序的最后幾頁(yè)寫(xiě)了一些自定的數(shù)據(jù),不希望在下載時(shí)被擦除,就可以將最后幾頁(yè)設(shè)置寫(xiě)保護(hù)鎖起來(lái)。注意,開(kāi)啟“寫(xiě)保護(hù)”后,若需要對(duì)保護(hù)區(qū)寫(xiě)入程序就會(huì)出錯(cuò)!并且該軟件不支持單獨(dú)寫(xiě)入選項(xiàng)字節(jié),只能在Flash下載時(shí)順便寫(xiě)入選項(xiàng)字節(jié),那也就是說(shuō),如果將Flash前幾頁(yè)寫(xiě)保護(hù)了,就再也無(wú)法使用FlyMcu下載程序了?。?/strong>(使用STLINK Utility可以補(bǔ)救回來(lái))
注意配置好選項(xiàng)字節(jié)數(shù)據(jù)后,點(diǎn)擊“采用這個(gè)設(shè)置”,并在FlyMcu主界面勾選“編程到FLASH時(shí)寫(xiě)選項(xiàng)字節(jié)”。

STLINK Utility需要安裝,安裝完成后,在桌面上存在快捷方式。STLINK Utility可以下載多種文件,包括HEX文件、BIN文件等。下面演示 下載程序的流程:
- 硬件接線。無(wú)需連接“USB轉(zhuǎn)串口模塊”,只需連接STLINK即可。BOOT引腳設(shè)置為 [BOOT1,BOOT0]=[0,0],然后按下復(fù)位鍵。點(diǎn)擊左起第三個(gè)按鈕進(jìn)行連接。
- 左起第一個(gè)按鈕:打開(kāi)程序文件,支持HEX格式、BIN格式。(也可以直接跳到下一步)
- 左起第六個(gè)按鈕:下載程序。跳過(guò)上一步的,此時(shí)選擇程序文件。然后點(diǎn)擊“Start”下載程序。下載完成后可以發(fā)現(xiàn)程序正常運(yùn)行。
選項(xiàng)字節(jié)的配置:菜單欄“Target”–>“Option Byte…”,下面依次介紹:
- 讀保護(hù)。
- 硬件參數(shù)?;疑倪x項(xiàng)就是本芯片不支持的選項(xiàng)。
- 用戶參數(shù)。
- 寫(xiě)保護(hù)。
注:配置好之后點(diǎn)擊“Apply”,就可以直接單獨(dú)更改選項(xiàng)字節(jié)的參數(shù)。文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-633964.html
左起第二個(gè)按鈕:讀芯片數(shù)據(jù),格式可以選為HEX格式、BIN格式。
左起第四個(gè)按鈕:斷開(kāi)連接。
左起第五個(gè)按鈕:擦除芯片。
STLINK固件更新:菜單欄“ST-LINK”–>Firmware update–>重新拔插STLINK–>點(diǎn)擊“Device Connect”–>“Yes>>>>”。文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-633964.html
到了這里,關(guān)于stm32學(xué)習(xí)筆記-9 USART串口的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!