|
STM32的串口通信 & Y! y- s3 D) S' Z E4 b# V* O% N
本文在于记录自己的学习过程中遇到的问题和总结,各种情况下串口通信在STM32的实际使用方面占有很大的比重,本文主要对串口通信做一个简要的总结。
2 W& C: [) [7 B3 W' W7 C一、STM32里的串口通信 在STM32里,串口通信是USART,STM32可以通过串口和其他设备进行传输并行数据,是全双工,异步时钟控制,设备之间是点对点的传输。
6 w0 ~$ F" H1 q" y0 h/ P3 e. e
对应的STM32引脚分别是RX和TX端。STM32的串口资源有USART1、USART2、USART3.
$ @: Z: v9 C* U" {
串口的几个重要的参数:
. W. H r1 K! x
波特率,串口通信的速率 . |& H. o& Y& [0 P1 C- v
空闲,一般为高电平
+ v7 l V' ^! T* W3 `/ s
起始位,标志一个数据帧的开始,固定为低电平。当数据开始发送时,产生一个下降沿。(空闲–>起始位) ( Q6 Q+ i" s! Z- F2 E& z6 N, U% l
数据位,发送数据帧,1为高电平,0为低电平。低位先行。 " ^0 B i K" B9 B w5 t
比如 发送数据帧0x0F 在数据帧里就是低位线性 即 1111 0000 2 [) Z( s- z) ]* O3 u6 f
校验位,用于数据验证,根据数据位的计算得来。有奇校验,偶校验和无校验。 / \1 x) t3 y( F! W( L6 ]" O1 `: I" U
停止位,用于数据的间隔,固定为高电平。数据帧发送完成后,产生一个上升沿。(数据传输–>停止位)
6 @0 T; G2 Q9 d8 q2 _: M
7 n7 F4 `4 Y4 _4 O: @( O
下方就是一个字节数据的传输过程,从图中可以看出,串口发送的数据一般都是以数据帧的形式进行传输,每个数据帧都由起始位,数据位,停止位组成, 且停止位可变。
1 s- f5 m( e/ ~* z+ c% s1 f
% O# K T4 J2 S) _5 ]& i3 R
' E6 @& T. V5 p, y) b
* M3 v8 ^+ u# b# {0 d
二、串口的发送和接收 USART是STM32内部集成的硬件外设,可以根据数据寄存器的一个字节数据自动生成数据帧时序,从TX引脚发送出去,也可以自动接收RX引脚的数据帧时序,拼接成一个字节数据,存放在数据寄存器里。 & E7 n% Y8 S+ G0 y; A2 z
当配置好USART的电路之后,直接读取数据寄存器,就可以自动发送数据和接收数据了。在发送和接收的模块有4个重要的寄存器
/ U- q/ s. h% e0 ^0 @7 ]. I
" j1 T( x4 M; J. J( y. w! o
下方为串口的发送和接收图解: L2 a' @4 ?$ Y6 {8 o
2 P2 o% k. I2 O' g+ T/ L5 i* q" d
; \5 ~+ t3 O6 h4 r5 w/ f0 Y
串口发送 4 N6 T- v7 f* Y
在配置串口的各个参数时,可以选择发送数据帧的数据位的大小,可选8位或9位。
' h& w( O8 v' }; y
串口发送数据实际上就是对发送数据寄存器TDR进行写操作。
' d+ L2 _ a9 W) V7 s
1. 当串口发送数据时,会检测发送移位寄存器是不是有数据正在移位,如果没有移位,那么这个数据就会立刻转移到发送移位寄存器里。准备发送。 ; z; a0 A' X- @
2. 当数据移动到移位寄存器时,会产生一个TXE发送寄存器空标志位,该位描述如下。当TXE被置1,那么就可以在TDR写入下一个数据了。即发送下一个数据。
1 Z. s. a! s# S
" z+ \0 V. t8 H8 G! D" E* s; B
7 \0 P/ ^+ E' u* K0 v& u
3. 发送移位寄存器在发送器控制的控制下,向右移位,一位一位的把数据传输到TX引脚。 , V2 W( V; R& b, W1 d1 I. R
( {6 U, p* L1 _5 U
+ T0 j" i1 A/ T9 A. C' u
4. 数据移位完成后,新的数据就会再次从TDR转移到发送移位寄存器里来,依次重复1-3的过程。通过读取TXE标志位来判断是否发送下一个数据。 ; T' m8 `2 z8 C- r+ [! v& C
串口接收
i& u/ i G3 W0 k2 k/ N6 ]" A
数据从RX引脚通向接收移位寄存器,在接收控制的控制下,一位一位的读取RX的电平,把第一位放在最高位,然后右移,移位八次之后就可以接收一个字节了。
7 A7 ]& a9 [6 P! N6 d) @
当一个字节数据移位完成之后,这一个字节的数据就会整体的移到接收数据寄存器RDR里来。
1 ]2 e& L/ A, [$ m
在转移时会置RXNE接收标志位,即RDR寄存器非空,下方为该位的描述。当被置1后,就说明数据可以被读出。 O6 `- R' r+ c. K5 @
! w6 ]+ o' z) B/ c
9 V; c$ V$ U' N$ w- S6 f
下图即为串口接收的工作流程
e; |1 z4 Y; v4 P0 e) _/ L# i
2 a7 B9 k: q# e' J v, }7 I5 H5 R
4 s7 d9 a4 o+ P- h5 e" u) m% Q
三、串口在STM32中的配置 0 N4 P" y, D8 Z$ ~: E$ d
首先要明确几点:使用STM32串口外设中的哪一个?串口发送或者接收数据?串口相关的参数配置?发送或接收是否使用到中断? ' R `" {, @6 L' \& e! E& c' f
下方为串口发送的配置。 5 E3 D+ e# C& L5 t" }$ x: O
1. RCC开启USART、串口TX/RX所对应的GPIO口 . Z/ a! M+ }! _' B9 [
- RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2,ENABLE); //开启USART2的时钟
& G7 y5 { q- P0 Q0 ?6 P - RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //开启GPIOA的时钟
复制代码
( F5 w! y- S* X1 ^! G
2. 初始化GPIO口 & C% `5 R5 V2 Y1 [ y! o6 m
这里注意哈,根据自己的需求来配置GPIO口,发送和接收是都需要还是只需要其中一个。然后对应的根据引脚定义表来初始化对应的GPIO口。
# A# [ g& X4 d# N! f _. m' F
USART2对应的引脚
# ^ M0 p) U7 y% b
$ D& V( Q9 C$ z9 W! W: p
: k* l/ d4 f9 h6 l
USART1对应的引脚 2 `- N# g" t7 ^, |; N( }
; B$ V% |2 a" D0 { L
6 Y* S, D5 G: U! A
这里根据手册来看,RX引脚模式配置成浮空输入或者上拉输入。TX引脚模式配置成复用推挽输出。
/ L% Z5 `3 c) M **比如我这里只初始化TX发送端**- //TX端% `2 T1 A9 V7 m$ Q8 ?
- GPIO_InitTypeDef GPIO_InitStructure;
2 e; t5 Y, g2 d4 u- G6 V - GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_PP; //复用推挽输出3 i- N! w2 D8 C" o T% c+ G
- GPIO_InitStructure.GPIO_Pin=GPIO_Pin_2; //USART2对应的TX端为GPIOA2! p+ M6 p1 J! X7 G
- GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz; //50MhZ% v8 u; j' z8 s/ K1 B: a7 E
- GPIO_Init(GPIOA,&GPIO_InitStructure);
复制代码
& ~4 x( A S) N% J# N5 v
3. 串口初始化
% ?8 m4 @: d3 S6 O: R/ N
注意哈,USART_Init()这个函数,是用来配置串口的相关参数的。
$ A& }8 R1 w# p
USART_BaudRate 串口通信使用的波特率 一般是9600或者是115200,这里我们给9600 ' `% P# N5 O* M3 u* s
USART_HardwareFlowControl 是否选择硬件流触发,一般这个我们也不选,所以选择无硬件流触发。 " a1 ]* i, N/ V8 N
USART_Mode 这个参数要注意了哈,串口的模式,发送模式还是接收模式,还是两者都有
9 h8 ]7 P) y$ J% b) s
USART_Parity 校验位,可以选择奇偶校验和不校验。没有需求就直接无校验 4 h" K3 @6 \+ K* D0 a3 u2 x! M
USART_StopBits 停止位 有1、0.5、2位,我们这里选1位停止位 % R& I. m# s t1 D/ ] W
USART_WordLength 数据位 有8位和9位可以选择 0 \' @1 F* P2 N* g, S
- //串口初始化
2 s% R6 S: x2 b3 g0 T; j - USART_InitTypeDef USART_InitStruct;
2 g7 H; N0 C& ?8 N - USART_StructInit(&USART_InitStruct); //初始默认值' B% f- y0 D1 p7 U; E6 X7 I+ D
- USART_InitStruct.USART_BaudRate=9600;3 u0 i) N0 M2 x& e. q
- USART_InitStruct.USART_HardwareFlowControl=USART_HardwareFlowControl_None; //不使用硬件流触发8 n2 d- F A1 m8 _4 C
- USART_InitStruct.USART_Mode=USART_Mode_Tx; //TX 发送模式
$ g- }/ F8 [) p2 T( ^% M - USART_InitStruct.USART_Parity=USART_Parity_No; //不选择校验
4 K: N& y! k5 \0 {. ?1 b3 E - USART_InitStruct.USART_StopBits=USART_StopBits_1; //停止位1位- q) w7 X, b9 ^- u
- USART_InitStruct.USART_WordLength=USART_WordLength_8b; //数据位8位
' o; v) E5 j2 q7 I A# u - USART_Init(USART2,&USART_InitStruct);
复制代码
5 |" E6 D0 z# \+ w' e* J& Q
4. 串口使能 - //串口使能
& o: ^/ c1 `$ q; t6 o - USART_Cmd(USART2,ENABLE);
复制代码) R6 t- V( x2 p8 E$ v
5. 串口发送数据
9 E4 b4 D( J1 ]注意哈,我们要判断TXE标志位的状态。0,数据还没有被转移到移位寄存器;1,数据已经被转移到移位寄存器。
. T* N# d2 g- N
当TXE标志位为1时,就说明可以发送下一个数据了。详细过程可看上面串口发送的解释。 - void Serial_SendByte(uint16_t Byte)
0 U% J/ \" n5 L - {
; T' N* K: u1 y& r! u - USART_SendData(USART2,Byte);8 O- B# r3 k" t$ I
- //0 表示数据还未转移到移位寄存器 循环等待 1 数据已经被转移到了移位寄存器可以发送数据
9 w* Q) V( H. L7 W" `- b! m. X X - while(USART_GetFlagStatus(USART2,USART_FLAG_TXE)==RESET); //不需要手动清零 再次写入TDR时会自动清零 ?% W" y. f3 r
- }
复制代码/ C& ]2 O2 I( |+ N
经过上述五步的配置,单片机就可以通过串口发送数据了。
2 E* s$ q# f; [+ S( a2 R
下方为发送数据图例,STM32向串口发送0x16数据 " ?3 l4 r6 Q) ]4 f8 D5 G# N
7 b3 T J3 ]* m5 {4 b. d, z& E
# d a7 L% i3 s7 Y% \) M: C
7 L0 I6 e. `! G# `" A
四、串口接收的两种实现方式 上方是发送数据的例子,那么串口接收又该如何配置,又要在串口发送的例子上做哪些更改呢?
7 o0 b1 h$ l5 L" i0 M# j' s: r
这里我们可以通过查询或者中断的方式来进行接收数据的两种方式。
( j* }2 i* a# r, s/ s, @' s
查询方式就是通过不断的查询RXNE标志位,通过判断RXNE位的状态来确定数据是否接收。
: u! X$ V; j9 y0 j) P+ b; K3 D
中断方式就是通过配置接收输出控制通道,配置NVIC,在中断服务子函数里进行数据的接收。
# t8 a0 y _+ @- |0 C' d5 U3 ?7 D) @
[+ W0 a. U6 J5 u9 ^
1. 需要更改的地方
2 t9 r4 \0 V! M( t! [, ]9 D. D" R3 C. y
既然我们要实现串口的接收,那么就要配置串口RX引脚,在串口模式中添加USART_Mode_RX模式。 : }- a6 S H. H( Z9 S! V
初始化RX引脚
' V. X' r' g0 G# W
- //RX端
- f6 Q. b* W" W" R. M" J8 c - GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IPU; //上拉输入
6 e( i5 v5 A8 P3 H3 _ - GPIO_InitStructure.GPIO_Pin=GPIO_Pin_3;
# a+ V( R, Y2 P, u2 y - GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz; //50MhZ
& I( i/ C2 c8 G$ R1 s J$ W - GPIO_Init(GPIOA,&GPIO_InitStructure);
复制代码 2 O3 e) S0 w* p. J7 e6 ~- y" R4 { ^9 @
& \' O# y' K7 s
- USART_InitStruct.USART_Mode=USART_Mode_Tx|USART_Mode_Rx; //TX 发送模式 RX 接收模式
复制代码
3 l+ \" n7 \1 q0 S. v
% s3 W9 b/ Z# [ B
2. 查询RXNE标志位
4 u6 l0 u; z2 m- G) f2 d( L' \
这里我们还是来看一看RXNE标志位的描述 8 A9 I- D5 B$ C) @8 g
9 l$ E+ Q! }4 @' T1 F
8 H9 W- {, R4 q+ l5 N3 {. R: F8 K
上图描述,为0时数据没有收到,为1时收到了数据,数据可以从RDR里读出 + b, L N3 r8 W1 J
所以在主程序里不断读取RXNE标志位,如果为1,表示数据可以读出 - uint8_t RX_Data;0 T* [# l% t& P
- int main()* h$ q4 r9 A$ u' K
- { + i9 n7 S: s8 g: ?" L( a
- Serial_Init();0 n2 ]9 \' }: L7 r
- Serial_SendByte(0x16);
: H1 t' p* f+ \, n. `2 m4 D! b' | - while(1)
' E9 c% b m/ u5 c - {9 ?* f1 d4 X' q$ |
- if(USART_GetFlagStatus(USART2,USART_FLAG_RXNE)==SET) //0 循环等待 1 可以接收数据4 r2 F5 L7 l6 k) O% e3 f. l, I5 }
- {* ~3 {3 }' V- |2 v0 m
- RX_Data=USART_ReceiveData(USART2);
, Y. V9 a0 y8 a8 `, h$ ~; a9 [ - Serial_SendByte(RX_Data);- |1 y0 o" m) \# g. {
- }" `0 a4 d- ^3 z# W
- }# H5 v7 S# h# x% s
- }
复制代码0 n* F0 b) z+ |0 r% F
下图为程序现象:pc向单片机发送数据0x15,单片机接收数据0x15,并且把接收到的数据作为数据发送到pc,在pc上显示0x15。
z5 X- V5 |+ E$ O
$ d( W8 x6 e* S. q9 ~4 }
6 |4 u2 c+ b: B6 J, T& ?
3. 使用中断
' ~8 k* z" D+ c7 k) C
- //开启中断输出控制
' ?, a2 \6 o+ j2 V7 @& \6 h - USART_ITConfig(USART2,USART_IT_RXNE,ENABLE);- D# A; Z# _) a2 [
-
( O0 o3 b# F! O% x. r; a - //配置NVIC
* ^- B$ p' w& A+ Z - NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
1 c8 ?" P. N i& x- K4 p - NVIC_InitTypeDef NVIC_InitStruct;/ p8 s8 S! f' [. b
- NVIC_InitStruct.NVIC_IRQChannel=USART2_IRQn; //选择USART2的中断通道
2 i3 ^& {/ S( `4 P - NVIC_InitStruct.NVIC_IRQChannelCmd=ENABLE; //中断使能
1 r2 |5 j5 o" @9 e$ \" D - NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority=1;
* n# M( Q1 O, s& O, Q - NVIC_InitStruct.NVIC_IRQChannelSubPriority=1;
' k |8 J* K! k9 _. i3 Y3 V) G - NVIC_Init(&NVIC_InitStruct);
复制代码
) z4 b$ I1 j& g0 f
: S) Y; l3 ^( C# Y
% l8 s+ |1 n5 @3 R! @
中断服务子函数写好后,就可以在中断里读取接收到的数据了。 1 u% `9 b/ L( Q
当接收到数据后,触发接收中断,主程序暂停执行。接收完数据后主程序回复执行。当接收到数据时,就触发中断。 - void USART2_IRQHandler(void)4 i' B, V# H& p0 W
- {. E+ @4 v' v+ Q3 k
- if(USART_GetITStatus(USART2,USART_IT_RXNE)==SET) //RXNE 标志位为1 表示可以接收数据
- J9 E8 g& g0 j, w# N - {: N1 X8 L5 L) E% p+ F& ~& P0 u
- RX_Data=USART_ReceiveData(USART2);, x6 E. I8 _4 d! X) d& Q
- Flag=1;7 k& R0 L# a# f# ]" ` R1 k
- USART_ClearITPendingBit(USART2,USART_IT_RXNE); //清除RXNE标志位
E3 J, p6 n" w1 E8 g& ? - }
% T/ E2 B q& r, ?& @1 [ - }
复制代码
! n9 W/ K4 i6 M+ ?* r @3 m- K主程序测试
5 n! d) h1 l' ?& A. f7 i% c
- uint8_t RX_Data;
9 ~0 Q% o" ^( G - uint8_t Flag;* O6 ]$ f# P, j+ A' W6 P) w
- ! L+ B( |) r: o0 K% H/ f \
- int main()
" x9 B0 ]. H9 e( p, u - {) y9 \: J1 H$ c% w+ e
- Serial_Init();4 N7 Q7 t5 _5 n L9 e0 D( X& M
- Serial_SendByte(0x16);
3 q/ W6 Q' H7 B3 L( `/ i1 Z U - : I' V# _0 J8 x" x7 E" @
- " x* D! R5 s* y1 q( ?) q. K
- while(1)
0 [0 U* ?- H+ m) w! U/ _, T - {- O; d# E4 F3 M2 L
- if(Flag==1)% n2 P" x' A, I* v/ o7 B8 R' c
- {3 l3 W L4 l$ _
- Serial_SendByte(RX_Data);; z& J( f. j2 ]7 t* l; A
- }
7 C6 k2 l. B$ i - }4 J3 B B3 _( c1 M- t: W) H
- }
# u% @$ C8 s' t1 C: `5 n - : X: k; M: h* R* \5 j
- void USART2_IRQHandler(void)2 }' C# n+ R# h
- {
k1 ~/ N# Y0 ^ - if(USART_GetITStatus(USART2,USART_IT_RXNE)==SET) //RXNE 标志位为1 表示可以接收数据
8 i! a" K$ N8 q9 K" z& H - {0 s A$ W5 ]9 O8 v. g0 a0 E2 f/ T# i
- RX_Data=USART_ReceiveData(USART2);5 G4 ?4 d( h: H) k( ?
- Flag=1;
1 ]8 w) q c6 F! S - USART_ClearITPendingBit(USART2,USART_IT_RXNE); //清除RXNE标志位
4 p% g: c6 }5 B3 @+ i1 q - }# H# G4 o. Y$ r# c: z9 w
- }
复制代码
" S- y1 W) @/ a- D6 l9 C1 X# I) ~
* m% a. N9 x3 C$ w1 P+ s
下图为程序现象:可以看到,串口确实收到了数据,只是我把接收到的数据0xFE放在了while循环里,这说明数据接收是成功的,使用中断是可行的。
% h% a2 J( @ n
4 V3 w/ j; s! X7 d5 l% z: B9 ?% L4 N. I9 }) ~/ X
1 Z& Q$ P( v% C+ j5 g( o- O h2 j总结 到这里,就大致总结了串口的发送和接收。 4 f1 j6 m# W4 @# g# L
串口的配置,使用查询或者中断来接收数据。 2 A( J! B4 b1 d+ w( W( H* g( h
串口的使用会很常用到,所以在这里对串口做一个总结,也算是对之前知识的一个回顾和总结,加强印象。 ) B0 V6 N' z; |) ^- t
如果有什么写的不对的地方,欢迎指正! 6 G3 E+ Z# R9 s6 R9 Q* Q4 c
转载自: 古月居 如有侵权请联系删除 2 }6 @, q: n1 X" p
5 q' T5 K# M& e4 j a
* P" Q( Y0 V( }0 c& _$ t |