STM32的串口通信
- [6 U3 ^3 H) r. D7 r+ d l
本文在于记录自己的学习过程中遇到的问题和总结,各种情况下串口通信在STM32的实际使用方面占有很大的比重,本文主要对串口通信做一个简要的总结。 0 i! p0 k: n) o* P6 ~
4 l/ C; F* c9 U0 \% w
一、STM32里的串口通信 - V( d+ u' k( J6 n
在STM32里,串口通信是USART,STM32可以通过串口和其他设备进行传输并行数据,是全双工,异步时钟控制,设备之间是点对点的传输。 + h. K1 C0 l0 B# g. C* ]; z( k t5 F0 W
对应的STM32引脚分别是RX和TX端。STM32的串口资源有USART1、USART2、USART3. / _7 h( i+ p; d0 V& o
串口的几个重要的参数: ! E7 q& M$ Y) m# X8 w: j
波特率,串口通信的速率 ! R: X3 D5 A V6 a/ T
空闲,一般为高电平
* p; B% l1 r% D* Q" L
起始位,标志一个数据帧的开始,固定为低电平。当数据开始发送时,产生一个下降沿。(空闲–>起始位) ; q( T: W w; L$ E
数据位,发送数据帧,1为高电平,0为低电平。低位先行。
' ] B0 ?( Y! N# T
比如 发送数据帧0x0F 在数据帧里就是低位线性 即 1111 0000
! s) ?- |: \2 }" B& [! R* |
校验位,用于数据验证,根据数据位的计算得来。有奇校验,偶校验和无校验。 # U6 Y: e9 e5 ]% b9 a2 U
停止位,用于数据的间隔,固定为高电平。数据帧发送完成后,产生一个上升沿。(数据传输–>停止位)
$ Z, E6 ^/ l, T3 b; A$ v4 O0 U6 A$ ]
* {' d* e9 N7 w/ c: x8 u
下方就是一个字节数据的传输过程,从图中可以看出,串口发送的数据一般都是以数据帧的形式进行传输,每个数据帧都由起始位,数据位,停止位组成, 且停止位可变。 4 i+ R& V( a8 g
z; L. w; M) O8 r4 l" m* Z
5 T& ?' A6 a6 ]" p# v
7 t8 r; U! O" Q/ w( ]
二、串口的发送和接收 ( E4 }1 _6 @4 J- G: t
USART是STM32内部集成的硬件外设,可以根据数据寄存器的一个字节数据自动生成数据帧时序,从TX引脚发送出去,也可以自动接收RX引脚的数据帧时序,拼接成一个字节数据,存放在数据寄存器里。 4 R# o8 p. F! U7 p G
当配置好USART的电路之后,直接读取数据寄存器,就可以自动发送数据和接收数据了。在发送和接收的模块有4个重要的寄存器 & S* J3 c! U6 F, `6 S; l# W
# y. t/ _9 e+ m3 k& I$ s2 C2 t1 u
下方为串口的发送和接收图解: . P1 ^* b+ J1 A& `1 L, N; s+ M
5 e$ l% ]& q* Q0 p: p2 p7 L N
% A/ i/ ?' ^" q6 i: k. w
串口发送
. x1 H# g ^; b( c- q
在配置串口的各个参数时,可以选择发送数据帧的数据位的大小,可选8位或9位。 9 ^% [) }5 |- s4 w
串口发送数据实际上就是对发送数据寄存器TDR进行写操作。 ! |3 R2 K0 V! J3 ^9 o
1. 当串口发送数据时,会检测发送移位寄存器是不是有数据正在移位,如果没有移位,那么这个数据就会立刻转移到发送移位寄存器里。准备发送。
1 B! {- S6 D2 R, k2 |
2. 当数据移动到移位寄存器时,会产生一个TXE发送寄存器空标志位,该位描述如下。当TXE被置1,那么就可以在TDR写入下一个数据了。即发送下一个数据。
4 x& `- R2 n" w! I; K& K
) } s8 {2 A7 H4 ?& S$ G% ?
1 y) g+ O/ s# c! W5 T1 o+ T5 G
3. 发送移位寄存器在发送器控制的控制下,向右移位,一位一位的把数据传输到TX引脚。
& A1 R, R% |8 A; q9 A
. v8 T% l' q, W% h) }6 M
; y1 e. `/ A) d# _0 H
4. 数据移位完成后,新的数据就会再次从TDR转移到发送移位寄存器里来,依次重复1-3的过程。通过读取TXE标志位来判断是否发送下一个数据。 1 C7 e& f1 k: i" O( i# H
串口接收
' V G& F' c$ i; _( a- V- N
数据从RX引脚通向接收移位寄存器,在接收控制的控制下,一位一位的读取RX的电平,把第一位放在最高位,然后右移,移位八次之后就可以接收一个字节了。 7 q j3 |+ i* M8 h; \) X/ L4 {2 V+ Z: m
当一个字节数据移位完成之后,这一个字节的数据就会整体的移到接收数据寄存器RDR里来。 , l" r- S6 k* P
在转移时会置RXNE接收标志位,即RDR寄存器非空,下方为该位的描述。当被置1后,就说明数据可以被读出。
/ h' f4 m) u4 ~$ t! g9 A
8 I. ^; C/ a, v4 M( h6 G$ Q
; S8 [9 U0 D1 G$ l& d5 \! H9 \* r
下图即为串口接收的工作流程 9 o% S* G: B1 |% e- Q5 J Y/ t
{+ s- \; X' K3 P9 l S8 M3 l( S/ w: S7 _
, \! n; d3 T! F. a5 R/ N2 Y& K! v6 e
三、串口在STM32中的配置 # q2 n, n4 s$ O
首先要明确几点:使用STM32串口外设中的哪一个?串口发送或者接收数据?串口相关的参数配置?发送或接收是否使用到中断? 4 d. `9 b1 Y4 M& y; Z1 {- d4 w
下方为串口发送的配置。 7 X+ M9 M2 k# ]& k2 C* Z$ m/ m
1. RCC开启USART、串口TX/RX所对应的GPIO口 - RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2,ENABLE); //开启USART2的时钟0 W: B. @( j# u/ a
- RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //开启GPIOA的时钟
复制代码
. n2 d @% N* a- G
2. 初始化GPIO口 V# w9 J/ s- C( K; [" W
这里注意哈,根据自己的需求来配置GPIO口,发送和接收是都需要还是只需要其中一个。然后对应的根据引脚定义表来初始化对应的GPIO口。 - w [" Q2 ~9 X
USART2对应的引脚
# M$ u6 ?3 y0 F1 {( a" [! v
* V- {& V* |& [$ N
# P+ ?( l2 h- | W% v
USART1对应的引脚 4 K. V( X) [. y1 U1 T8 n" t
1 {* ]! l6 T% q: l
" G2 O- q4 X' [3 Q" x' P- Z
这里根据手册来看,RX引脚模式配置成浮空输入或者上拉输入。TX引脚模式配置成复用推挽输出。 9 m9 G% b! e8 |. D3 I- x
**比如我这里只初始化TX发送端**- //TX端
# c+ ]6 d: z: R$ J: i R( Y/ k, N - GPIO_InitTypeDef GPIO_InitStructure;
# z* l( c) e/ W3 S - GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_PP; //复用推挽输出
7 K/ [+ p6 D6 B8 a& u - GPIO_InitStructure.GPIO_Pin=GPIO_Pin_2; //USART2对应的TX端为GPIOA2
3 f, L; @4 I9 G, w3 t - GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz; //50MhZ) T4 ]" f% O% [4 J, R0 _
- GPIO_Init(GPIOA,&GPIO_InitStructure);
复制代码% I( C0 f% Z }
3. 串口初始化 % V( q& ?. ]: j( x3 {- @0 X
注意哈,USART_Init()这个函数,是用来配置串口的相关参数的。 8 J7 a. K7 N, ]
USART_BaudRate 串口通信使用的波特率 一般是9600或者是115200,这里我们给9600 o# S8 v4 b- s1 |, O6 C! K
USART_HardwareFlowControl 是否选择硬件流触发,一般这个我们也不选,所以选择无硬件流触发。 ) S# }- n9 J$ \/ |
USART_Mode 这个参数要注意了哈,串口的模式,发送模式还是接收模式,还是两者都有 , v) h3 A- A( y$ I
USART_Parity 校验位,可以选择奇偶校验和不校验。没有需求就直接无校验 ) t' u$ M: }! C
USART_StopBits 停止位 有1、0.5、2位,我们这里选1位停止位 6 ?* N$ w S2 B* w7 b
USART_WordLength 数据位 有8位和9位可以选择 - //串口初始化# e- c4 y2 f( W3 e: b
- USART_InitTypeDef USART_InitStruct;
0 [- E9 [6 l/ X2 K/ T! @7 c( j# ? - USART_StructInit(&USART_InitStruct); //初始默认值4 A, f& D8 ^% [& k+ z" C
- USART_InitStruct.USART_BaudRate=9600;
5 f* w6 z, x& Q9 ] - USART_InitStruct.USART_HardwareFlowControl=USART_HardwareFlowControl_None; //不使用硬件流触发& K5 ^$ W* M& |
- USART_InitStruct.USART_Mode=USART_Mode_Tx; //TX 发送模式
$ {9 X6 t1 ^/ K& S5 m% W - USART_InitStruct.USART_Parity=USART_Parity_No; //不选择校验, {# y' b8 p, G! T! b
- USART_InitStruct.USART_StopBits=USART_StopBits_1; //停止位1位2 V2 s) i7 H3 e$ T
- USART_InitStruct.USART_WordLength=USART_WordLength_8b; //数据位8位
8 ?) l+ t7 q4 ^7 d4 _ L# j - USART_Init(USART2,&USART_InitStruct);
复制代码 : D/ l+ m2 I9 [% S( G, |0 r$ |) `
6 Q. Z; B! f! [9 [% O8 f2 a
4. 串口使能 - //串口使能
" ~$ b5 Z4 ~$ _7 c - USART_Cmd(USART2,ENABLE);
复制代码
7 Q5 u$ a' k! M8 K7 U2 Z
5. 串口发送数据 ; r! N1 t4 U7 n: y( H
注意哈,我们要判断TXE标志位的状态。0,数据还没有被转移到移位寄存器;1,数据已经被转移到移位寄存器。 ( R( w. q" C* }5 X" p& `5 _
当TXE标志位为1时,就说明可以发送下一个数据了。详细过程可看上面串口发送的解释。 - void Serial_SendByte(uint16_t Byte)$ x; }" B) K! P/ m. m
- {; W- J+ y& K' V, j
- USART_SendData(USART2,Byte);, @8 e s# I6 L' { J a
- //0 表示数据还未转移到移位寄存器 循环等待 1 数据已经被转移到了移位寄存器可以发送数据
3 N D9 o6 h. Y2 ^* X' @ - while(USART_GetFlagStatus(USART2,USART_FLAG_TXE)==RESET); //不需要手动清零 再次写入TDR时会自动清零 3 g) }0 {5 d# ^' ^6 T
- }
复制代码1 u. _0 Y' C2 D! W
经过上述五步的配置,单片机就可以通过串口发送数据了。
s' |& i" n$ F s
下方为发送数据图例,STM32向串口发送0x16数据
1 X6 H% u* U6 C. H3 G
4 T$ C! s$ m: J: V) _/ ^
& u S$ P; Y6 s1 ~& q; Q
四、串口接收的两种实现方式
& f6 s- Y6 W2 P) L上方是发送数据的例子,那么串口接收又该如何配置,又要在串口发送的例子上做哪些更改呢?
# e& a. G$ a2 a" I* B; }# X% T
这里我们可以通过查询或者中断的方式来进行接收数据的两种方式。 - V0 j( q0 L' z2 l! D
( t% o0 E! N. X. @- A' H- l
1. 需要更改的地方
3 R4 X% L/ e% B3 G, [' ~% i
; |# Q. K1 v5 i 既然我们要实现串口的接收,那么就要配置串口RX引脚,在串口模式中添加USART_Mode_RX模式。 # ^" I; d# n: d2 p- I
初始化RX引脚 - //RX端
# r$ K/ T1 e. n( c - GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IPU; //上拉输入
( N2 g/ E6 O+ B8 c; I - GPIO_InitStructure.GPIO_Pin=GPIO_Pin_3;
# L/ `: n8 v- n0 t+ s% H - GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz; //50MhZ/ Q; [- V) ~( p/ \
- GPIO_Init(GPIOA,&GPIO_InitStructure);
复制代码 , g, {/ v* L: `$ T1 R; H
) q% A8 w( C+ N" \: m4 x
添加串口模式 - USART_InitStruct.USART_Mode=USART_Mode_Tx|USART_Mode_Rx; //TX 发送模式 RX 接收模式
复制代码 7 x- R6 u4 B8 g' r
. j& Y% `' W8 ?
2. 查询RXNE标志位
, L. j* e; P/ k. m4 @8 @
这里我们还是来看一看RXNE标志位的描述 " W& L z7 ^! B! Z0 n
( F1 C8 T& p4 r! W7 k7 K$ c
i! J. K" u: E9 b
上图描述,为0时数据没有收到,为1时收到了数据,数据可以从RDR里读出
& o% Y- J' |- A5 `
所以在主程序里不断读取RXNE标志位,如果为1,表示数据可以读出 - uint8_t RX_Data;
4 N- Y9 J5 u/ U' d - int main(); r. Z1 P/ J8 m) J
- {
+ n: A+ l7 O. A7 L4 ]: G9 @ - Serial_Init();
9 |* S& ]0 s! e- ?, c, c% c* q - Serial_SendByte(0x16);0 k U G' ?! _3 {. \8 {5 _
- while(1)2 @9 U" c8 X/ N/ t$ D% _3 o9 i
- {
3 d. Y! a( K, ^/ ` - if(USART_GetFlagStatus(USART2,USART_FLAG_RXNE)==SET) //0 循环等待 1 可以接收数据
$ R1 w+ c* j9 n- y - {
2 V4 R$ V9 \8 g# s- a, P - RX_Data=USART_ReceiveData(USART2); % R# G2 w. E `/ J5 c
- Serial_SendByte(RX_Data);
+ E2 W/ v: I" N! I( D+ ]! {- @8 d- y M - }* f# b0 P. v7 C! n# t
- }
8 k! w9 @" @. w+ V. p/ @$ q+ Z! F - }
复制代码
8 @' t2 O+ t9 r
下图为程序现象:pc向单片机发送数据0x15,单片机接收数据0x15,并且把接收到的数据作为数据发送到pc,在pc上显示0x15。 , R$ V% C! p+ e% `+ M$ |3 g) } g
8 o: U3 ~! n; w1 n0 a
5 S \7 [- p' l) ?! v- g
3. 使用中断
: R3 q1 i# z) Y' u3 |
- //开启中断输出控制
9 d7 ]- K" J7 P - USART_ITConfig(USART2,USART_IT_RXNE,ENABLE);, T4 a: X1 ?8 A+ t0 D9 k; L
- : i, U7 R4 E* p2 s$ v
- //配置NVIC/ V S% Z) m' } x
- NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
9 P# Z* O+ W: A+ J' E9 z - NVIC_InitTypeDef NVIC_InitStruct;
* F5 l/ P5 c! s3 ~1 v5 {$ ^ - NVIC_InitStruct.NVIC_IRQChannel=USART2_IRQn; //选择USART2的中断通道
: J) y! p# l# L$ a5 i) c( ~0 p1 D3 H - NVIC_InitStruct.NVIC_IRQChannelCmd=ENABLE; //中断使能
( d1 M* z$ Q5 a% L - NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority=1;
4 E' r7 ]2 m3 l. V- ] - NVIC_InitStruct.NVIC_IRQChannelSubPriority=1;
: z: W3 T5 r" G/ z! f - NVIC_Init(&NVIC_InitStruct);
复制代码
- Z, J# M4 F" c% ]
中断服务子函数 $ C9 T9 j* H5 P9 T1 a
5 m: a: o+ K3 h6 l: z中断服务子函数写好后,就可以在中断里读取接收到的数据了。
' Y, A9 O/ W( ^" I1 p; ~3 M/ |当接收到数据后,触发接收中断,主程序暂停执行。接收完数据后主程序回复执行。当接收到数据时,就触发中断。
- void USART2_IRQHandler(void)
% V# l' K6 W6 r* v( }3 [9 X - {
+ a0 W% C+ G' [7 Q: ^ - if(USART_GetITStatus(USART2,USART_IT_RXNE)==SET) //RXNE 标志位为1 表示可以接收数据
1 _; A+ M' Y- a0 z/ ^ - {
b' g3 f+ `$ i; p) u" c% @ \ - RX_Data=USART_ReceiveData(USART2);+ H$ O6 C1 W6 e. b) K
- Flag=1;
" l% n, |# U3 t1 } - USART_ClearITPendingBit(USART2,USART_IT_RXNE); //清除RXNE标志位" u9 Z& `* ^1 F' E
- }, p4 G& E8 a ^) @9 F! X$ p0 E* ^
- }
复制代码8 K- p7 A6 c1 O; f% P
- uint8_t RX_Data;& {# x6 f) Z/ S: [
- uint8_t Flag;6 ^9 q% ^) F$ N8 O3 t @& P
- ( D1 x g5 d4 ^% \; O' K- g
- int main()( Q; V0 U9 E- R; u/ Z
- {( O! h% K) Y/ x" f
- Serial_Init();1 `' Q7 u6 O) H+ c# Y0 k3 a
- Serial_SendByte(0x16);
% J% M) r. B! _: R+ X8 h/ }5 M3 n
3 d9 ]9 a9 w1 I% h8 ~- 6 R1 W' u: h# X7 f# R, p) P4 _6 O
- while(1)
# i4 v; f% E$ L( F1 B" W - {2 h W X# o' n3 r& [
- if(Flag==1)8 ?6 t* X) j& p+ r
- {
) Y; b7 m& d4 u7 Z- D - Serial_SendByte(RX_Data);6 Z- u: G9 t3 S! D. z7 ?
- }
}, a5 h; G/ \ - }
8 D/ v8 N4 Z! H! Q2 V - }
& J% I* k7 g9 F2 V7 {" ?* t- u( e
; Q3 n( _7 X K$ J, ^+ e! R% p2 p6 J- void USART2_IRQHandler(void)
) {! V8 s" c) j: |* ] - {
1 t/ W8 \4 L# ^: C. S7 G( t$ b! q/ O1 U - if(USART_GetITStatus(USART2,USART_IT_RXNE)==SET) //RXNE 标志位为1 表示可以接收数据
+ k5 d g, A; Y5 S - {
+ K* r0 G- H1 e - RX_Data=USART_ReceiveData(USART2);
4 X/ v2 _" K3 w0 N+ j1 s1 W A - Flag=1;+ l V2 Y( z& |; D( c3 o
- USART_ClearITPendingBit(USART2,USART_IT_RXNE); //清除RXNE标志位
: d1 I `* M$ J1 x+ L - }
5 K2 b6 T5 Q/ O6 _& e+ y+ r. y - }
复制代码
6 a( w9 E @/ c( G2 B) f. M
: S% J9 Z0 m) [$ v! b) q, }: q3 ^
下图为程序现象:可以看到,串口确实收到了数据,只是我把接收到的数据0xFE放在了while循环里,这说明数据接收是成功的,使用中断是可行的。
: `& v) Q' K7 U) ^7 \+ g1 ?
) V7 L& x. `% r. W7 a/ @8 v1 X; {
' z8 P2 l6 K5 Z; \! |$ ^ * q6 i1 X* w2 b& ]1 U0 v/ N
总结
# Q- E0 d) D+ t
到这里,就大致总结了串口的发送和接收。
: ~) n2 ]6 B2 v
串口的配置,使用查询或者中断来接收数据。 4 S' ?3 _9 s9 t
串口的使用会很常用到,所以在这里对串口做一个总结,也算是对之前知识的一个回顾和总结,加强印象。 $ E7 z; E- `8 d
转载自: 古月居 如有侵权请联系删除
- h8 d6 u _- ~2 q* B
0 c2 X, ~5 t* }' z9 S |