STM32的串口通信 # D2 d- C7 q: _4 N: r4 W; U3 `; Z
本文在于记录自己的学习过程中遇到的问题和总结,各种情况下串口通信在STM32的实际使用方面占有很大的比重,本文主要对串口通信做一个简要的总结。
" D- [" h) [: S
2 i- K# u6 r# Y+ N0 t" O( M) q+ m
一、STM32里的串口通信 " w7 y5 R, o" N+ G; ^' o/ p8 w
在STM32里,串口通信是USART,STM32可以通过串口和其他设备进行传输并行数据,是全双工,异步时钟控制,设备之间是点对点的传输。
, ~0 |% [# \( }$ a! j* k+ I- g
对应的STM32引脚分别是RX和TX端。STM32的串口资源有USART1、USART2、USART3. 9 G; B) Y3 ~% F% x: r& a0 d& E$ X
串口的几个重要的参数:
3 O, l" [3 @) |, E% z v
波特率,串口通信的速率 & E3 O/ L5 T- i- g
空闲,一般为高电平
" t2 v( n# ]$ u
起始位,标志一个数据帧的开始,固定为低电平。当数据开始发送时,产生一个下降沿。(空闲–>起始位)
7 n/ A9 L( X* M" Z3 |6 Y
数据位,发送数据帧,1为高电平,0为低电平。低位先行。
) D& c! _5 ~. g, f/ m. z, H/ y
比如 发送数据帧0x0F 在数据帧里就是低位线性 即 1111 0000
# [4 B6 i- }, m0 \# S, A
校验位,用于数据验证,根据数据位的计算得来。有奇校验,偶校验和无校验。 . j" b- R6 f# @, j+ O& ~2 C" r" T; |$ ?
停止位,用于数据的间隔,固定为高电平。数据帧发送完成后,产生一个上升沿。(数据传输–>停止位) ; {: V+ B, e$ X
3 F2 l: _* b. B0 Y, M) r: p- a1 z
下方就是一个字节数据的传输过程,从图中可以看出,串口发送的数据一般都是以数据帧的形式进行传输,每个数据帧都由起始位,数据位,停止位组成, 且停止位可变。
) H1 w+ f5 S# m. a6 j @5 q, L
8 d |0 D' M0 H% V0 Y; y
3 q5 f3 v+ q3 i, w: R
0 l/ C' Q0 [# p$ s
二、串口的发送和接收
' x7 X) h0 @: MUSART是STM32内部集成的硬件外设,可以根据数据寄存器的一个字节数据自动生成数据帧时序,从TX引脚发送出去,也可以自动接收RX引脚的数据帧时序,拼接成一个字节数据,存放在数据寄存器里。
% G+ l: L7 w+ E8 Y1 g. J
当配置好USART的电路之后,直接读取数据寄存器,就可以自动发送数据和接收数据了。在发送和接收的模块有4个重要的寄存器
' ]3 O& B' D# G( r) P9 i F0 ?
发送数据寄存器TDR 7 }$ p4 b/ x) l1 Z/ y
发送移位寄存器,把一个字节的数据一位一位的移出去
2 ~" h/ ^/ H' V: D8 o; @
接收数据寄存器RDR . u7 r/ m' I, p5 d
接收移位寄存器,把一个字节的数据 % Y8 p5 K2 K- V7 s
: @6 ^8 m" F# b8 O# y
下方为串口的发送和接收图解: ) f$ D: J* r( M7 J. R) g7 d; o
, B' Q' h- i8 M
, h/ ]" L1 C7 n. {! j# \. x$ z
串口发送
& r7 g, d, d& Z D
在配置串口的各个参数时,可以选择发送数据帧的数据位的大小,可选8位或9位。
- P4 d7 ~5 d0 Z# e# W: L' G
串口发送数据实际上就是对发送数据寄存器TDR进行写操作。
# F( O' t- _8 t' D
1. 当串口发送数据时,会检测发送移位寄存器是不是有数据正在移位,如果没有移位,那么这个数据就会立刻转移到发送移位寄存器里。准备发送。 1 d% l( g: M$ f9 x2 R
2. 当数据移动到移位寄存器时,会产生一个TXE发送寄存器空标志位,该位描述如下。当TXE被置1,那么就可以在TDR写入下一个数据了。即发送下一个数据。
* d% [! m" F- u+ @
# Q5 t4 V C1 Q! t1 c8 C& B) y4 ]5 D: R. t
3. 发送移位寄存器在发送器控制的控制下,向右移位,一位一位的把数据传输到TX引脚。 $ ?$ M; h! {1 Q* p: g: |
1 l# X9 B/ p/ I0 X4 A- E
) p- ?0 \8 R& m9 g
4. 数据移位完成后,新的数据就会再次从TDR转移到发送移位寄存器里来,依次重复1-3的过程。通过读取TXE标志位来判断是否发送下一个数据。
4 ?/ A8 O# O- a! B
串口接收 5 w8 u+ C7 D6 x1 ~# g/ P. Z
数据从RX引脚通向接收移位寄存器,在接收控制的控制下,一位一位的读取RX的电平,把第一位放在最高位,然后右移,移位八次之后就可以接收一个字节了。
h( E+ {; z8 T' s5 A; o
当一个字节数据移位完成之后,这一个字节的数据就会整体的移到接收数据寄存器RDR里来。 ) H; B, _6 C& a+ R5 [( l x
在转移时会置RXNE接收标志位,即RDR寄存器非空,下方为该位的描述。当被置1后,就说明数据可以被读出。
% e6 @5 D2 Z7 Z- ~$ f: s5 P$ u! o; R
" G y7 K* ^- V6 B
* \0 L a# O' n
下图即为串口接收的工作流程
5 f+ a3 {! V7 \2 l+ P; g
/ m7 G, X& a9 M" Y
2 j7 d" d$ n; G$ R7 [3 W' T% I- `! A! z
三、串口在STM32中的配置 . i, l, h: w2 }: Y
首先要明确几点:使用STM32串口外设中的哪一个?串口发送或者接收数据?串口相关的参数配置?发送或接收是否使用到中断?
4 p" n1 d- ^* h; y* o& F3 Y
下方为串口发送的配置。
$ M( d4 T" o2 v. y: v( Y+ @) E
1. RCC开启USART、串口TX/RX所对应的GPIO口 - RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2,ENABLE); //开启USART2的时钟
5 \- `& B+ N9 ^! l - RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //开启GPIOA的时钟
复制代码
+ W0 v8 }, d* U2 J) M! r
2. 初始化GPIO口
" L- i @) l1 q0 a C; K
这里注意哈,根据自己的需求来配置GPIO口,发送和接收是都需要还是只需要其中一个。然后对应的根据引脚定义表来初始化对应的GPIO口。 & }# t8 ?7 Q+ B9 @( K4 E5 y
USART2对应的引脚 # Q1 U1 \0 a7 m2 A
" e- v6 ^& r' ?
6 s. f) R- D( m ?3 q% n b
USART1对应的引脚 $ o& i$ S3 W( M$ w3 h
$ H6 k, x/ s, Y, X/ p; c' j
/ p3 b" A- U) i( c, A) s- W4 `
这里根据手册来看,RX引脚模式配置成浮空输入或者上拉输入。TX引脚模式配置成复用推挽输出。 8 m, B7 i3 i% Z. W5 N! }
**比如我这里只初始化TX发送端**- //TX端" Z0 b% X/ l5 l& j
- GPIO_InitTypeDef GPIO_InitStructure;8 b2 b+ n! ]5 Z& J! g9 L, J
- GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_PP; //复用推挽输出3 p; w# [' m5 Y/ J6 d6 h
- GPIO_InitStructure.GPIO_Pin=GPIO_Pin_2; //USART2对应的TX端为GPIOA29 U+ r$ `( h9 m3 ]' q
- GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz; //50MhZ
8 S4 w9 I3 F4 X+ P- U; h) H - GPIO_Init(GPIOA,&GPIO_InitStructure);
复制代码
' _8 {* E S$ `; C5 R5 o% Z" ~" p
3. 串口初始化 ! W) M9 W( l# q( S# d1 K6 c$ z
注意哈,USART_Init()这个函数,是用来配置串口的相关参数的。 . }7 o' n1 f' ^0 a+ M
USART_BaudRate 串口通信使用的波特率 一般是9600或者是115200,这里我们给9600 + b, f% p+ }) _+ b2 X2 a- X5 f
USART_HardwareFlowControl 是否选择硬件流触发,一般这个我们也不选,所以选择无硬件流触发。
* }7 x; z2 f. [. m( I& h0 ~% M7 v
USART_Mode 这个参数要注意了哈,串口的模式,发送模式还是接收模式,还是两者都有
% T( N! R% \. ~ `: S& B$ ] B
USART_Parity 校验位,可以选择奇偶校验和不校验。没有需求就直接无校验
) g0 q5 B0 o/ H) C, g' T
USART_StopBits 停止位 有1、0.5、2位,我们这里选1位停止位 0 v2 Z( ?2 U/ @, x9 f$ B P" S u& y
USART_WordLength 数据位 有8位和9位可以选择 - //串口初始化
! t7 s! M% X4 J6 g% r - USART_InitTypeDef USART_InitStruct;
& G1 d4 l3 M. f - USART_StructInit(&USART_InitStruct); //初始默认值
R1 D" o- y& Y0 [+ a - USART_InitStruct.USART_BaudRate=9600;
! B n( S0 p% |6 l% c - USART_InitStruct.USART_HardwareFlowControl=USART_HardwareFlowControl_None; //不使用硬件流触发3 n" v' o8 {6 u* s9 ^+ ]
- USART_InitStruct.USART_Mode=USART_Mode_Tx; //TX 发送模式
& y* F3 s0 I' e - USART_InitStruct.USART_Parity=USART_Parity_No; //不选择校验5 f* V7 l; U1 g3 S$ @- k
- USART_InitStruct.USART_StopBits=USART_StopBits_1; //停止位1位
3 P4 f: s" N4 I" t - USART_InitStruct.USART_WordLength=USART_WordLength_8b; //数据位8位
& ]% C+ E' C! l. ~- ^. @4 \! z3 e - USART_Init(USART2,&USART_InitStruct);
复制代码 7 T* K3 j8 l$ s" g1 T% N
; O: M, Y' V9 i; D$ G
4. 串口使能 - //串口使能5 D" Z! p, f: _
- USART_Cmd(USART2,ENABLE);
复制代码
( s2 C$ O$ d* R8 N9 k
5. 串口发送数据 ( U, u5 u8 [; k# s Q% `$ i7 C
注意哈,我们要判断TXE标志位的状态。0,数据还没有被转移到移位寄存器;1,数据已经被转移到移位寄存器。
6 L+ M* o2 G0 d) \1 H* @4 E+ \
当TXE标志位为1时,就说明可以发送下一个数据了。详细过程可看上面串口发送的解释。 - void Serial_SendByte(uint16_t Byte)" T+ E6 T+ J0 M4 I8 e8 l1 y9 ~
- {+ U. R- x0 Z5 \# \/ q1 U9 F, Q
- USART_SendData(USART2,Byte);! F5 w2 O+ V+ N6 y- S
- //0 表示数据还未转移到移位寄存器 循环等待 1 数据已经被转移到了移位寄存器可以发送数据& ~2 \5 v, q# Z8 [: S) h
- while(USART_GetFlagStatus(USART2,USART_FLAG_TXE)==RESET); //不需要手动清零 再次写入TDR时会自动清零
0 Z# g9 w5 o$ \7 Z3 z! Y! o - }
复制代码
6 M8 _9 D. w0 \+ W" _
经过上述五步的配置,单片机就可以通过串口发送数据了。 ; i0 l/ f" }7 X/ R, q" `
下方为发送数据图例,STM32向串口发送0x16数据 Y: g9 k4 ]/ q9 A* z4 L
* K: E) z8 H2 i; U" g3 P
9 u' h8 b- m2 a0 |* ]1 |
四、串口接收的两种实现方式
9 P. o3 o6 v, {上方是发送数据的例子,那么串口接收又该如何配置,又要在串口发送的例子上做哪些更改呢?
) b- z, F2 M; G
这里我们可以通过查询或者中断的方式来进行接收数据的两种方式。
+ s; t1 T( y9 ?0 I
查询方式就是通过不断的查询RXNE标志位,通过判断RXNE位的状态来确定数据是否接收。
2 I9 O; |' ^: R# O3 U# B& U+ G% S
中断方式就是通过配置接收输出控制通道,配置NVIC,在中断服务子函数里进行数据的接收。
. m9 E8 R/ e$ q4 p z! |2 M4 y
- p% x* }8 E# a! ~
1. 需要更改的地方
3 B7 l+ f& i7 z7 y" e8 o
4 B- Z7 k0 h. W- T& K a0 R 既然我们要实现串口的接收,那么就要配置串口RX引脚,在串口模式中添加USART_Mode_RX模式。
3 N$ x' z$ k! d& g
初始化RX引脚 - //RX端
, ?0 ~! P- U1 i - GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IPU; //上拉输入6 g/ _, ]+ G6 Y: M
- GPIO_InitStructure.GPIO_Pin=GPIO_Pin_3;
, z5 [3 Q) e4 G2 G3 m5 ~! S+ P - GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz; //50MhZ
+ \8 l i& T! ~% u2 K$ @! L - GPIO_Init(GPIOA,&GPIO_InitStructure);
复制代码
4 D& U5 [) ^1 {& R! T1 O4 _
- g; P) x+ O9 O6 `/ G
添加串口模式 - USART_InitStruct.USART_Mode=USART_Mode_Tx|USART_Mode_Rx; //TX 发送模式 RX 接收模式
复制代码
% U p4 H5 r. b1 G. t
/ @3 n) }; P) n3 x
2. 查询RXNE标志位 ! Z* ]$ m. Z: m# h8 g
这里我们还是来看一看RXNE标志位的描述
1 R2 D/ o. N6 ]/ T: G
- J) s# p& h# W7 V2 o: R) V+ z* \1 A. Z: z
上图描述,为0时数据没有收到,为1时收到了数据,数据可以从RDR里读出
7 }5 w- t$ i9 q6 N2 I9 o( [$ S
所以在主程序里不断读取RXNE标志位,如果为1,表示数据可以读出 - uint8_t RX_Data;
; v5 l' C* Y' Y - int main()
, F# N f* Y& K5 } - { ) `9 o& e: \, B. G( [5 O. g1 o/ w j4 }! A
- Serial_Init();
: d2 f4 t* G! y5 j( p - Serial_SendByte(0x16);
. R8 `, B9 G9 h0 E M/ I - while(1) Q* V, P- P' e( o; n) x/ c5 c
- {1 b5 L, E3 W6 \9 e: o4 }
- if(USART_GetFlagStatus(USART2,USART_FLAG_RXNE)==SET) //0 循环等待 1 可以接收数据
8 h7 L5 }% i9 A - {. F0 k/ B' Y0 o$ x* N4 W
- RX_Data=USART_ReceiveData(USART2); 4 J" P1 f3 J0 G3 {: \
- Serial_SendByte(RX_Data);
* n: ]1 D' \. h* q/ S+ _: a/ I& \% z - }
* d2 U4 v- c, u4 Z, ]7 z - }
! Y0 w+ Y; J# D( ? - }
复制代码8 J! ?9 M! z7 S& H' C U( W- z
下图为程序现象:pc向单片机发送数据0x15,单片机接收数据0x15,并且把接收到的数据作为数据发送到pc,在pc上显示0x15。 2 J) m6 v. q( e$ j
9 r1 h$ j0 P+ w, [ p
* O+ K+ ~. h! N& s
3. 使用中断 , G' m' m _- D' P
- //开启中断输出控制
9 P1 X# l7 y# N# ~, A - USART_ITConfig(USART2,USART_IT_RXNE,ENABLE);
1 n/ d% B' K* A9 |0 B% z - 4 @' J/ E9 ^+ G, J
- //配置NVIC }1 [) ^: w9 }/ g* W
- NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
- G+ J( W* }# c6 w - NVIC_InitTypeDef NVIC_InitStruct;6 {/ g- O0 U+ E8 P/ r- r' N* p
- NVIC_InitStruct.NVIC_IRQChannel=USART2_IRQn; //选择USART2的中断通道
3 S4 U5 \% D8 m9 Y - NVIC_InitStruct.NVIC_IRQChannelCmd=ENABLE; //中断使能( y7 I1 H% J- P. p
- NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority=1;
+ V% M6 b4 N4 c4 A/ \ - NVIC_InitStruct.NVIC_IRQChannelSubPriority=1;& Q* y+ B4 q* g7 U5 Q# u2 D H
- NVIC_Init(&NVIC_InitStruct);
复制代码
( Z) H6 `, q" P* m- T. y
中断服务子函数 0 k# `8 v% l9 `+ K- o4 f3 h' p
0 c" E, f5 G9 R7 a; D
中断服务子函数写好后,就可以在中断里读取接收到的数据了。 % g2 C9 c8 G' ]
当接收到数据后,触发接收中断,主程序暂停执行。接收完数据后主程序回复执行。当接收到数据时,就触发中断。 - void USART2_IRQHandler(void)
i5 M5 f$ W, G( R9 o2 y7 \ - {
9 c7 T6 L, O$ v [+ `$ D& O8 w - if(USART_GetITStatus(USART2,USART_IT_RXNE)==SET) //RXNE 标志位为1 表示可以接收数据
! |3 j$ |5 {+ i/ G) K; R& s( m. O+ N - {! }: ~5 X9 t$ G6 E0 O7 d) G
- RX_Data=USART_ReceiveData(USART2);) d) H& b. z. e0 s
- Flag=1;- l. t. ?$ D n3 E5 l
- USART_ClearITPendingBit(USART2,USART_IT_RXNE); //清除RXNE标志位
. k( D0 @5 ?$ s; b0 u5 C' G2 n/ m - }: T7 _% ?# I' F& p: R
- }
复制代码+ W9 f9 ]9 m7 w J. V
- uint8_t RX_Data;
5 x# G% s) c/ I* D - uint8_t Flag;* z, y1 j/ s2 d7 h2 o9 Q. G
- 5 N2 k" S+ w& O3 q6 d- H; Q+ l/ D4 F" G
- int main()
( X# J/ P$ l( p - {
" R; Q8 K) f7 N2 x3 |$ v8 r1 [ - Serial_Init();
7 C( q, b. O- D$ O& S/ T' z2 z - Serial_SendByte(0x16);) ^, D1 v V/ M' |; T: q
- / X/ P: t# g Q9 \
- / Z2 i6 Y& E- s+ b
- while(1)
1 ]& u/ C3 N, ` - { `. K5 F2 o) Y
- if(Flag==1)" `, L+ ~/ e- O8 @
- {, t$ P* H; j2 D c- l
- Serial_SendByte(RX_Data);
* r* e+ y& a& Z% `% ^ - }7 X! W5 U; S3 \9 b! @ K; Y
- }
! M2 i# R0 _+ k5 K I - }. d2 A$ a. y% W) y" X; R; r
0 Z. t# F% v) I6 L# E# D- void USART2_IRQHandler(void)
8 B* f% {, b( @ - {: N7 G3 C' c, e. n, ]( T+ Z# o. A
- if(USART_GetITStatus(USART2,USART_IT_RXNE)==SET) //RXNE 标志位为1 表示可以接收数据
2 V: T8 d. f2 e6 M& o% W - {3 F1 @, K* j2 ^
- RX_Data=USART_ReceiveData(USART2);& u& e# a; S K
- Flag=1;; r* \5 z0 P# S! V2 m7 R
- USART_ClearITPendingBit(USART2,USART_IT_RXNE); //清除RXNE标志位0 v V3 A/ B/ J1 ]1 a, F
- }# _) P k' E1 |6 o4 I
- }
复制代码 / A% ^+ r: f x
) n. _5 b+ e4 i" _ \0 R% o
下图为程序现象:可以看到,串口确实收到了数据,只是我把接收到的数据0xFE放在了while循环里,这说明数据接收是成功的,使用中断是可行的。
1 n5 m2 D5 {2 T7 K; P! K9 T, W
8 t# \5 N L( D9 s# q
, o; ?3 f4 r( U n" e. U) Y! c. \% } 4 |; _4 K1 s# h8 v
总结 8 o a0 B/ o) Y& i$ s( |( s
到这里,就大致总结了串口的发送和接收。
4 z' Z- p9 _5 n! m( C, r, L
串口的配置,使用查询或者中断来接收数据。 & L6 E' X! Y7 U H4 ^3 Z$ u
串口的使用会很常用到,所以在这里对串口做一个总结,也算是对之前知识的一个回顾和总结,加强印象。 0 E, h2 T% Y6 o) A" L$ X
转载自: 古月居 如有侵权请联系删除
8 p* y$ r5 p% {2 P 9 }" t: A- V: j& s
|