STM32的串口通信 + f9 G, v- _! n3 \/ l
本文在于记录自己的学习过程中遇到的问题和总结,各种情况下串口通信在STM32的实际使用方面占有很大的比重,本文主要对串口通信做一个简要的总结。 0 n% U) z. P* Z ^& H) V5 B4 c
& k0 |; Q" M; m1 `
一、STM32里的串口通信
L# b% F5 {! P" G' ]* l, v$ j5 N
在STM32里,串口通信是USART,STM32可以通过串口和其他设备进行传输并行数据,是全双工,异步时钟控制,设备之间是点对点的传输。
- O' r1 c# X* c" ]! W- j
对应的STM32引脚分别是RX和TX端。STM32的串口资源有USART1、USART2、USART3. 8 Y+ ]: t" P6 L- | L
串口的几个重要的参数:
% B8 d9 Z- ]- F' v% V1 R
波特率,串口通信的速率 6 D, j2 |# K2 T9 E: j, Z" R# \" k2 G
空闲,一般为高电平
F1 m4 p' m* q( T$ f
起始位,标志一个数据帧的开始,固定为低电平。当数据开始发送时,产生一个下降沿。(空闲–>起始位)
- {, J1 b3 {: W3 n
数据位,发送数据帧,1为高电平,0为低电平。低位先行。
( M: e0 k8 R& G
比如 发送数据帧0x0F 在数据帧里就是低位线性 即 1111 0000
; b B0 b: \. M0 E* J r# V- U
校验位,用于数据验证,根据数据位的计算得来。有奇校验,偶校验和无校验。
2 A {0 `1 \$ I2 k4 k0 K1 L2 J, n
停止位,用于数据的间隔,固定为高电平。数据帧发送完成后,产生一个上升沿。(数据传输–>停止位) . A/ }/ ^" E! x) A c
- i% F! s7 q% E
下方就是一个字节数据的传输过程,从图中可以看出,串口发送的数据一般都是以数据帧的形式进行传输,每个数据帧都由起始位,数据位,停止位组成, 且停止位可变。
; E- J' A+ S; q1 A) e( H
, h" v) \- |( k4 t7 {
* X E2 ?* ], ~( ?$ c% x7 e7 V% c' R. E
二、串口的发送和接收
7 y" a5 s9 z1 Z( V1 B" o0 eUSART是STM32内部集成的硬件外设,可以根据数据寄存器的一个字节数据自动生成数据帧时序,从TX引脚发送出去,也可以自动接收RX引脚的数据帧时序,拼接成一个字节数据,存放在数据寄存器里。 0 c" L Y( e& {- [6 E, I4 \
当配置好USART的电路之后,直接读取数据寄存器,就可以自动发送数据和接收数据了。在发送和接收的模块有4个重要的寄存器
% o/ I9 K1 Q5 e( t" s
发送数据寄存器TDR
4 `! t6 s7 ?/ V9 }5 @
发送移位寄存器,把一个字节的数据一位一位的移出去 8 m( a. H' z0 s- |6 @% q
接收数据寄存器RDR / V. s7 L" D z( \6 W
接收移位寄存器,把一个字节的数据
% X1 C) T1 ^) [# ]
9 m7 M. {( b" x& J, n3 |
下方为串口的发送和接收图解: 7 M* l, @! ^2 l4 Z- T
) L7 a! ~6 z8 H) e! ^
5 M8 i0 J! Q8 _3 B% d h' J2 q
串口发送 5 H0 h4 M% p0 C' G. }: k% r) A+ O4 u
在配置串口的各个参数时,可以选择发送数据帧的数据位的大小,可选8位或9位。 7 q* V, N5 ~5 M. m9 w3 n- Q
串口发送数据实际上就是对发送数据寄存器TDR进行写操作。
8 s6 U# `8 h5 C N2 b d" E
1. 当串口发送数据时,会检测发送移位寄存器是不是有数据正在移位,如果没有移位,那么这个数据就会立刻转移到发送移位寄存器里。准备发送。
8 w1 O# v% z) W4 g$ P0 C
2. 当数据移动到移位寄存器时,会产生一个TXE发送寄存器空标志位,该位描述如下。当TXE被置1,那么就可以在TDR写入下一个数据了。即发送下一个数据。
( E, f! a4 e w6 L F7 z) h K
" u5 A# j- S/ q+ Q6 ~ h4 \& n! z% _; p* O6 y
3. 发送移位寄存器在发送器控制的控制下,向右移位,一位一位的把数据传输到TX引脚。 - N4 ? L. S$ N% }% B
, x4 `6 ]" d( C, u1 }, ~
( L' O! O: X* }
4. 数据移位完成后,新的数据就会再次从TDR转移到发送移位寄存器里来,依次重复1-3的过程。通过读取TXE标志位来判断是否发送下一个数据。 : z* `: p' N0 \2 t3 c
串口接收 2 ^+ P* e% U- B- N
数据从RX引脚通向接收移位寄存器,在接收控制的控制下,一位一位的读取RX的电平,把第一位放在最高位,然后右移,移位八次之后就可以接收一个字节了。 4 V- s4 _% Y! {/ O" z" _# W7 w
当一个字节数据移位完成之后,这一个字节的数据就会整体的移到接收数据寄存器RDR里来。 8 _+ X+ J3 F/ G4 g9 @( c/ Q
在转移时会置RXNE接收标志位,即RDR寄存器非空,下方为该位的描述。当被置1后,就说明数据可以被读出。 # o/ E6 z5 G" U: u' j
5 u9 X3 W- L/ W, m( L- }* f4 D$ ^ k
+ }) J8 S- x* e, p( f
下图即为串口接收的工作流程
* }* {* e! h( V& O5 e' P- v
0 V3 |9 M+ l6 M% I
: b1 q! W" u. I( z3 ^% r
" t3 K! q0 @1 j r三、串口在STM32中的配置 0 C, ~; D' j; s1 }" w6 v# S
首先要明确几点:使用STM32串口外设中的哪一个?串口发送或者接收数据?串口相关的参数配置?发送或接收是否使用到中断?
% i. `- j0 \6 C4 H. P8 u5 _3 j6 J0 S
下方为串口发送的配置。 2 s6 D5 {" S, o9 k: S; e5 v; e
1. RCC开启USART、串口TX/RX所对应的GPIO口 - RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2,ENABLE); //开启USART2的时钟# U7 h# @8 S. G5 |" s# w
- RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //开启GPIOA的时钟
复制代码& S, o6 k! @) K) p
2. 初始化GPIO口 1 U# p5 C& v7 @; f* Y6 F
这里注意哈,根据自己的需求来配置GPIO口,发送和接收是都需要还是只需要其中一个。然后对应的根据引脚定义表来初始化对应的GPIO口。 ! m, z' [/ K" e! Z) `
USART2对应的引脚
4 I& U3 V6 v2 d% g0 `1 F/ J8 n
5 z3 `" j. R$ s2 O
9 b4 k$ Y8 o$ Y& u& G) n* C
USART1对应的引脚
. v4 q5 @" F6 |$ Z9 G% b
7 x8 r2 D' v& @
$ }( y$ d: J8 I z2 c! P6 Q# W
这里根据手册来看,RX引脚模式配置成浮空输入或者上拉输入。TX引脚模式配置成复用推挽输出。 , n) U4 Y( J+ R4 j# c
**比如我这里只初始化TX发送端**- //TX端
0 R* Y- [5 D3 r% j+ ^+ e% _6 N+ F - GPIO_InitTypeDef GPIO_InitStructure;
% ]& D5 j1 O2 P! P, E - GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_PP; //复用推挽输出
. V, U- ^7 d9 Y2 ] - GPIO_InitStructure.GPIO_Pin=GPIO_Pin_2; //USART2对应的TX端为GPIOA2
\2 c2 r$ u7 c* `6 H( I - GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz; //50MhZ
2 N z: a m: i - GPIO_Init(GPIOA,&GPIO_InitStructure);
复制代码% l' b! @# t* S5 `7 W$ ~* R
3. 串口初始化 * U' V8 [! A; H0 r9 B' l. P
注意哈,USART_Init()这个函数,是用来配置串口的相关参数的。 ' D! {3 ^" D4 z s" d; m: d1 `
USART_BaudRate 串口通信使用的波特率 一般是9600或者是115200,这里我们给9600
# i, C# ^: ^. ]' Q1 _
USART_HardwareFlowControl 是否选择硬件流触发,一般这个我们也不选,所以选择无硬件流触发。
+ d' J+ G9 n3 T/ e& [4 g
USART_Mode 这个参数要注意了哈,串口的模式,发送模式还是接收模式,还是两者都有 * i+ e% m# X( H4 {' j
USART_Parity 校验位,可以选择奇偶校验和不校验。没有需求就直接无校验
2 [+ b: c5 G0 Y! x. x
USART_StopBits 停止位 有1、0.5、2位,我们这里选1位停止位
7 A' k$ O1 k' S3 z2 I! t
USART_WordLength 数据位 有8位和9位可以选择 - //串口初始化
# w7 V7 A+ E$ A" i" ?) t - USART_InitTypeDef USART_InitStruct;% P m$ z& R$ w! Y$ Z P& W4 ^0 p
- USART_StructInit(&USART_InitStruct); //初始默认值
5 i) Z5 o: `0 t: w4 c - USART_InitStruct.USART_BaudRate=9600;! E/ M- Z! U( T% _5 w
- USART_InitStruct.USART_HardwareFlowControl=USART_HardwareFlowControl_None; //不使用硬件流触发) o0 E+ `' N& x+ g' T3 w
- USART_InitStruct.USART_Mode=USART_Mode_Tx; //TX 发送模式 v" d: b7 \- }
- USART_InitStruct.USART_Parity=USART_Parity_No; //不选择校验; S' O, I0 G9 W& c- |: `
- USART_InitStruct.USART_StopBits=USART_StopBits_1; //停止位1位9 O( R9 e. F$ v \* }
- USART_InitStruct.USART_WordLength=USART_WordLength_8b; //数据位8位
, A# f7 v' |/ m/ n) s4 s - USART_Init(USART2,&USART_InitStruct);
复制代码
6 q2 p. C& _6 o; F8 E0 I5 N) |
/ X' p' {/ g8 Z) Q
4. 串口使能 - //串口使能 W& x* a& Q7 m
- USART_Cmd(USART2,ENABLE);
复制代码) z9 y J! j7 \+ Y0 F
5. 串口发送数据
6 x$ N8 O' z7 u$ b* ?注意哈,我们要判断TXE标志位的状态。0,数据还没有被转移到移位寄存器;1,数据已经被转移到移位寄存器。
! ?+ W9 S$ I) \$ S
当TXE标志位为1时,就说明可以发送下一个数据了。详细过程可看上面串口发送的解释。 - void Serial_SendByte(uint16_t Byte)$ }' L3 ` J. K4 y5 s( [7 |& M
- {
% G4 \" P; p# _8 X - USART_SendData(USART2,Byte);8 ?3 A3 ? q2 ?0 B& X1 a
- //0 表示数据还未转移到移位寄存器 循环等待 1 数据已经被转移到了移位寄存器可以发送数据
) A [3 R( h6 }. R- p - while(USART_GetFlagStatus(USART2,USART_FLAG_TXE)==RESET); //不需要手动清零 再次写入TDR时会自动清零 6 Z" u0 X9 o8 @' }: I. }
- }
复制代码
/ D$ Y7 Q/ f3 z, O
经过上述五步的配置,单片机就可以通过串口发送数据了。
' o z* N$ ]' ^
下方为发送数据图例,STM32向串口发送0x16数据 ( u, g% P- j# k/ a; x
4 Y* G0 s, G* S
5 |8 r. x+ x1 U- B- `: v6 k
四、串口接收的两种实现方式 ) y3 v. }" F6 a) P
上方是发送数据的例子,那么串口接收又该如何配置,又要在串口发送的例子上做哪些更改呢? 3 v0 _: o4 N! u& z
这里我们可以通过查询或者中断的方式来进行接收数据的两种方式。 . M9 }" Q6 |; V$ ^: j: M
8 C; w( i5 S& |0 `) z1 w% K
1. 需要更改的地方9 n- w# s% L: y( z- u# F/ W) M
; @; l2 Y! v3 p; `+ x) o
既然我们要实现串口的接收,那么就要配置串口RX引脚,在串口模式中添加USART_Mode_RX模式。 . T# @4 l. T7 w( Y- b
初始化RX引脚 - //RX端; g; l+ e! C) C5 `; B
- GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IPU; //上拉输入 j0 K7 g" z" l' l6 u
- GPIO_InitStructure.GPIO_Pin=GPIO_Pin_3;- p/ g" K$ ]0 h5 g. m
- GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz; //50MhZ
5 T. v* n" k# }% w - GPIO_Init(GPIOA,&GPIO_InitStructure);
复制代码 ) n$ c) Z1 s2 M7 l& s
' d! [: a3 P7 ]4 O7 V
添加串口模式 - USART_InitStruct.USART_Mode=USART_Mode_Tx|USART_Mode_Rx; //TX 发送模式 RX 接收模式
复制代码
% O$ v. G" G8 ?2 ^* d& B0 U
# j8 M" P# l2 L; B9 X2 W% m; d
2. 查询RXNE标志位
+ _2 ~/ }5 Q* B6 \
这里我们还是来看一看RXNE标志位的描述 * q. u @+ y! P: Q% ^/ X8 f
' j# @ q; ]1 {0 [
& A7 {: B# _( E& I8 ?7 g
上图描述,为0时数据没有收到,为1时收到了数据,数据可以从RDR里读出
: Y: H" e7 |. L; C5 ~
所以在主程序里不断读取RXNE标志位,如果为1,表示数据可以读出 - uint8_t RX_Data;- E: V+ y r, }2 v$ J
- int main()
( |) G5 [- r' S2 `1 N( W - {
' @6 Y+ I0 X: e. e# b/ w5 t7 z - Serial_Init();$ G' _' o- g. N# J9 m% S) J* Y
- Serial_SendByte(0x16);" ~: _ y! J4 _% z! ^5 W+ l o
- while(1)3 H: H5 C6 ~) L3 D
- {9 v |; h8 p8 z E
- if(USART_GetFlagStatus(USART2,USART_FLAG_RXNE)==SET) //0 循环等待 1 可以接收数据7 T, I" I6 w# C2 y( z) M7 i% M
- {7 g2 d7 |% O; v
- RX_Data=USART_ReceiveData(USART2); $ Z6 r9 b0 R1 Z. a8 M$ K; |
- Serial_SendByte(RX_Data);+ b; ]7 Y5 ^9 D' r
- }
% q% C0 X9 {) ]. a8 N - }
$ |1 e2 Q; _- z& ]( U7 `- } - }
复制代码! u3 R# x* N4 Y, E
下图为程序现象:pc向单片机发送数据0x15,单片机接收数据0x15,并且把接收到的数据作为数据发送到pc,在pc上显示0x15。
7 L5 E8 @* R1 {" D( K( ]
, [* M3 P! t; f g+ ~
1 H4 N2 Y2 P( T/ e2 V
3. 使用中断 6 G) T/ x% r: ~4 {- H! |- k
- //开启中断输出控制
% ]1 s! @* `# _9 Y - USART_ITConfig(USART2,USART_IT_RXNE,ENABLE);: |. S! q& Z* G1 j
-
! W, X u; ]8 o+ q% s6 _! f - //配置NVIC& m" `) Y: w- D D9 T, U
- NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); - J1 ~! \/ e0 |% r2 @! N, a
- NVIC_InitTypeDef NVIC_InitStruct;
2 t Y' z% M6 {$ } O - NVIC_InitStruct.NVIC_IRQChannel=USART2_IRQn; //选择USART2的中断通道
$ c( t o+ A5 W; N8 N5 y2 t7 Y - NVIC_InitStruct.NVIC_IRQChannelCmd=ENABLE; //中断使能
9 T5 G8 E8 r2 U. Y6 r; o - NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority=1;
$ Z# T( r2 H$ A$ V - NVIC_InitStruct.NVIC_IRQChannelSubPriority=1;: p9 A; x* v& i6 T- [7 d0 c; {) k4 X
- NVIC_Init(&NVIC_InitStruct);
复制代码6 |3 }( j% `, @0 Y6 G: B
中断服务子函数 * p" V$ |$ Q7 m; H
. J( D, p. w( v2 F3 ?
中断服务子函数写好后,就可以在中断里读取接收到的数据了。 : o8 J6 a/ {$ ~, r. ~% L. b; B: @
当接收到数据后,触发接收中断,主程序暂停执行。接收完数据后主程序回复执行。当接收到数据时,就触发中断。 - void USART2_IRQHandler(void)
. p7 i1 u( m& w7 P: ]- s - {
: G7 M% T/ J$ |' J) T" | - if(USART_GetITStatus(USART2,USART_IT_RXNE)==SET) //RXNE 标志位为1 表示可以接收数据4 c Q. b7 h7 I
- {
2 o: {! F0 E/ n1 C - RX_Data=USART_ReceiveData(USART2);
& |0 N7 @/ O( ^ - Flag=1;, s) {6 \/ o `$ O
- USART_ClearITPendingBit(USART2,USART_IT_RXNE); //清除RXNE标志位
8 R) k* i$ q4 h N' s - }
% j' [" Z' W8 S, T. A7 J - }
复制代码
- ^; z3 W; @9 _( H4 G$ s1 r
主程序测试
) f* k) t$ P: x& o v, ~( A9 T R1 c
- uint8_t RX_Data;; S) _* ^4 f/ e5 S
- uint8_t Flag;% J+ A; E8 G* g. Y0 P
7 I+ y& A# k. w- |8 X& X6 P( ]- int main()
x: x: g% J. W$ X - {
6 }1 c5 y! N4 [6 w* y - Serial_Init();
# [5 Q6 E4 J7 f5 B - Serial_SendByte(0x16);& @ b3 S2 }9 T r4 |+ n
- ) g- z" a4 y0 k4 t. g
3 L9 E; V0 T% {- while(1)
2 c7 }+ u/ |3 l$ l5 X - {3 E5 L( R; a! o! j) G+ ^
- if(Flag==1)! |0 V6 m7 o+ T
- {, F7 Y$ g: t; \0 D
- Serial_SendByte(RX_Data);
7 Y5 g1 x/ K% d: f3 q7 B9 b - }
1 R/ Q# ^- n- M- n - }
! o: R9 I) ~, q5 S+ D4 f - }
T& w$ s/ v4 ~8 V% { - 6 k; s1 W ?, D1 F3 j3 T- e& {% i
- void USART2_IRQHandler(void)
* k! I2 X+ \; n - {
7 {, g4 J1 c; x$ a$ M; a - if(USART_GetITStatus(USART2,USART_IT_RXNE)==SET) //RXNE 标志位为1 表示可以接收数据4 t: Q( h& o3 Q' [) n! }
- {! x) n9 s, K# k
- RX_Data=USART_ReceiveData(USART2);
k% y1 c; f3 \- ?" _ - Flag=1;4 S2 C- T$ i' g- o* p8 {: p' d% B7 y+ j
- USART_ClearITPendingBit(USART2,USART_IT_RXNE); //清除RXNE标志位
' D3 b! a2 m) R w. L. k9 t/ X - }- o* W: B2 {* C' E5 G: E; Y! R3 {
- }
复制代码
* Q4 u _ @* ^. a' J3 \: b4 [ d# Q* x# ]2 s' }- m. p
下图为程序现象:可以看到,串口确实收到了数据,只是我把接收到的数据0xFE放在了while循环里,这说明数据接收是成功的,使用中断是可行的。 7 a" g$ P0 {3 @7 g2 \5 \# n
! C) K; z$ g4 T1 d0 |% y' ~
2 }% a, i: c+ ]5 R6 n1 a
4 l% R* y" g, ], i) N4 V( |# @总结 5 S3 v" `, X+ W7 W% B) _
到这里,就大致总结了串口的发送和接收。
6 s w( E* \4 A! T
串口的配置,使用查询或者中断来接收数据。
$ Q* }, k$ X9 K
串口的使用会很常用到,所以在这里对串口做一个总结,也算是对之前知识的一个回顾和总结,加强印象。
$ @# k$ S C4 x
转载自: 古月居 如有侵权请联系删除
$ d4 K4 ?8 W8 ]: R% k& a+ f1 N/ B
. G2 m' K1 m/ P2 o: _& Z8 o5 m& E |