你的浏览器版本过低,可能导致网站不能正常访问!
为了你能正常使用网站功能,请使用这些浏览器。

基于STM32的串口通信经验分享

[复制链接]
攻城狮Melo 发布时间:2023-8-10 14:48
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
微信图片_20230810144703_3.png 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 @: M
USART是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
微信图片_20230810144703_2.png , 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+ @
微信图片_20230810144703_1.png
# Q5 t4 V  C1 Q
! t1 c8 C& B) y4 ]5 D: R. t
3. 发送移位寄存器在发送器控制的控制下,向右移位,一位一位的把数据传输到TX引脚。
$ ?$ M; h! {1 Q* p: g: |
微信图片_20230810144703.png
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
微信图片_20230810144704_3.png " G  y7 K* ^- V6 B
* \0 L  a# O' n
下图即为串口接收的工作流程

5 f+ a3 {! V7 \2 l+ P; g
微信图片_20230810144704_2.png
/ 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口
  1. RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2,ENABLE);  //开启USART2的时钟
    5 \- `& B+ N9 ^! l
  2. 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
微信图片_20230810144704_1.png " e- v6 ^& r' ?
6 s. f) R- D( m  ?3 q% n  b
USART1对应的引脚
$ o& i$ S3 W( M$ w3 h
微信图片_20230810144704.png
$ 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发送端**
  1. //TX端" Z0 b% X/ l5 l& j
  2.     GPIO_InitTypeDef GPIO_InitStructure;8 b2 b+ n! ]5 Z& J! g9 L, J
  3.     GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_PP;  //复用推挽输出3 p; w# [' m5 Y/ J6 d6 h
  4.     GPIO_InitStructure.GPIO_Pin=GPIO_Pin_2;   //USART2对应的TX端为GPIOA29 U+ r$ `( h9 m3 ]' q
  5.     GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;  //50MhZ
    8 S4 w9 I3 F4 X+ P- U; h) H
  6.     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位可以选择
    1. //串口初始化
      ! t7 s! M% X4 J6 g% r
    2.   USART_InitTypeDef USART_InitStruct;
      & G1 d4 l3 M. f
    3.   USART_StructInit(&USART_InitStruct);  //初始默认值
        R1 D" o- y& Y0 [+ a
    4.   USART_InitStruct.USART_BaudRate=9600;
      ! B  n( S0 p% |6 l% c
    5.   USART_InitStruct.USART_HardwareFlowControl=USART_HardwareFlowControl_None;   //不使用硬件流触发3 n" v' o8 {6 u* s9 ^+ ]
    6.   USART_InitStruct.USART_Mode=USART_Mode_Tx;         //TX 发送模式
      & y* F3 s0 I' e
    7.   USART_InitStruct.USART_Parity=USART_Parity_No;       //不选择校验5 f* V7 l; U1 g3 S$ @- k
    8.   USART_InitStruct.USART_StopBits=USART_StopBits_1;      //停止位1位
      3 P4 f: s" N4 I" t
    9.   USART_InitStruct.USART_WordLength=USART_WordLength_8b;  //数据位8位
      & ]% C+ E' C! l. ~- ^. @4 \! z3 e
    10.   USART_Init(USART2,&USART_InitStruct);
    复制代码
    7 T* K3 j8 l$ s" g1 T% N

; O: M, Y' V9 i; D$ G
4. 串口使能
  1. //串口使能5 D" Z! p, f: _
  2.    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时,就说明可以发送下一个数据了。详细过程可看上面串口发送的解释。
  1. void Serial_SendByte(uint16_t Byte)" T+ E6 T+ J0 M4 I8 e8 l1 y9 ~
  2. {+ U. R- x0 Z5 \# \/ q1 U9 F, Q
  3.   USART_SendData(USART2,Byte);! F5 w2 O+ V+ N6 y- S
  4.   //0 表示数据还未转移到移位寄存器 循环等待 1 数据已经被转移到了移位寄存器可以发送数据& ~2 \5 v, q# Z8 [: S) h
  5.   while(USART_GetFlagStatus(USART2,USART_FLAG_TXE)==RESET);  //不需要手动清零 再次写入TDR时会自动清零  
    0 Z# g9 w5 o$ \7 Z3 z! Y! o
  6. }
复制代码

6 M8 _9 D. w0 \+ W" _
经过上述五步的配置,单片机就可以通过串口发送数据了。
; i0 l/ f" }7 X/ R, q" `
下方为发送数据图例,STM32向串口发送0x16数据
  Y: g9 k4 ]/ q9 A* z4 L
微信图片_20230810144705_3.png * 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引脚
    1. //RX端
      , ?0 ~! P- U1 i
    2.   GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IPU;  //上拉输入6 g/ _, ]+ G6 Y: M
    3.     GPIO_InitStructure.GPIO_Pin=GPIO_Pin_3;
      , z5 [3 Q) e4 G2 G3 m5 ~! S+ P
    4.     GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;  //50MhZ
      + \8 l  i& T! ~% u2 K$ @! L
    5.     GPIO_Init(GPIOA,&GPIO_InitStructure);
    复制代码

    4 D& U5 [) ^1 {& R! T1 O4 _

- g; P) x+ O9 O6 `/ G
  • 添加串口模式
    1. 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
微信图片_20230810144705_2.png
- 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,表示数据可以读出
  1. uint8_t RX_Data;
    ; v5 l' C* Y' Y
  2. int main()
    , F# N  f* Y& K5 }
  3. { ) `9 o& e: \, B. G( [5 O. g1 o/ w  j4 }! A
  4.     Serial_Init();
    : d2 f4 t* G! y5 j( p
  5.     Serial_SendByte(0x16);
    . R8 `, B9 G9 h0 E  M/ I
  6.     while(1)  Q* V, P- P' e( o; n) x/ c5 c
  7.     {1 b5 L, E3 W6 \9 e: o4 }
  8.         if(USART_GetFlagStatus(USART2,USART_FLAG_RXNE)==SET)   //0 循环等待 1 可以接收数据
    8 h7 L5 }% i9 A
  9.         {. F0 k/ B' Y0 o$ x* N4 W
  10.             RX_Data=USART_ReceiveData(USART2);           4 J" P1 f3 J0 G3 {: \
  11.       Serial_SendByte(RX_Data);
    * n: ]1 D' \. h* q/ S+ _: a/ I& \% z
  12.         }
    * d2 U4 v- c, u4 Z, ]7 z
  13.     }
    ! Y0 w+ Y; J# D( ?
  14. }
复制代码
8 J! ?9 M! z7 S& H' C  U( W- z
下图为程序现象:pc向单片机发送数据0x15,单片机接收数据0x15,并且把接收到的数据作为数据发送到pc,在pc上显示0x15。
2 J) m6 v. q( e$ j
微信图片_20230810144705_1.png 9 r1 h$ j0 P+ w, [  p

* O+ K+ ~. h! N& s
3. 使用中断
, G' m' m  _- D' P
  • 通过配置串口的接收作为中断源,开启中断输出控制,配置NVIC。开启中断通道。

    ' t" @; p$ ^2 n! b" M, J( \  u
  1. //开启中断输出控制
    9 P1 X# l7 y# N# ~, A
  2.   USART_ITConfig(USART2,USART_IT_RXNE,ENABLE);
    1 n/ d% B' K* A9 |0 B% z
  3.   4 @' J/ E9 ^+ G, J
  4.   //配置NVIC  }1 [) ^: w9 }/ g* W
  5.   NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    - G+ J( W* }# c6 w
  6.   NVIC_InitTypeDef NVIC_InitStruct;6 {/ g- O0 U+ E8 P/ r- r' N* p
  7.   NVIC_InitStruct.NVIC_IRQChannel=USART2_IRQn;   //选择USART2的中断通道
    3 S4 U5 \% D8 m9 Y
  8.   NVIC_InitStruct.NVIC_IRQChannelCmd=ENABLE;    //中断使能( y7 I1 H% J- P. p
  9.   NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority=1;
    + V% M6 b4 N4 c4 A/ \
  10.   NVIC_InitStruct.NVIC_IRQChannelSubPriority=1;& Q* y+ B4 q* g7 U5 Q# u2 D  H
  11.   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' ]
当接收到数据后,触发接收中断,主程序暂停执行。接收完数据后主程序回复执行。当接收到数据时,就触发中断。
  1. void USART2_IRQHandler(void)
      i5 M5 f$ W, G( R9 o2 y7 \
  2. {
    9 c7 T6 L, O$ v  [+ `$ D& O8 w
  3.   if(USART_GetITStatus(USART2,USART_IT_RXNE)==SET)   //RXNE 标志位为1 表示可以接收数据
    ! |3 j$ |5 {+ i/ G) K; R& s( m. O+ N
  4.   {! }: ~5 X9 t$ G6 E0 O7 d) G
  5.     RX_Data=USART_ReceiveData(USART2);) d) H& b. z. e0 s
  6.     Flag=1;- l. t. ?$ D  n3 E5 l
  7.     USART_ClearITPendingBit(USART2,USART_IT_RXNE);  //清除RXNE标志位
    . k( D0 @5 ?$ s; b0 u5 C' G2 n/ m
  8.   }: T7 _% ?# I' F& p: R
  9. }
复制代码
+ W9 f9 ]9 m7 w  J. V
  • 主程序测试
    ! j$ a" e6 z, `4 t
  1. uint8_t RX_Data;
    5 x# G% s) c/ I* D
  2. uint8_t Flag;* z, y1 j/ s2 d7 h2 o9 Q. G
  3. 5 N2 k" S+ w& O3 q6 d- H; Q+ l/ D4 F" G
  4. int main()
    ( X# J/ P$ l( p
  5. {
    " R; Q8 K) f7 N2 x3 |$ v8 r1 [
  6.     Serial_Init();
    7 C( q, b. O- D$ O& S/ T' z2 z
  7.     Serial_SendByte(0x16);) ^, D1 v  V/ M' |; T: q
  8. / X/ P: t# g  Q9 \
  9. / Z2 i6 Y& E- s+ b
  10.     while(1)
    1 ]& u/ C3 N, `
  11.     {  `. K5 F2 o) Y
  12.         if(Flag==1)" `, L+ ~/ e- O8 @
  13.         {, t$ P* H; j2 D  c- l
  14.             Serial_SendByte(RX_Data);
    * r* e+ y& a& Z% `% ^
  15.         }7 X! W5 U; S3 \9 b! @  K; Y
  16.     }
    ! M2 i# R0 _+ k5 K  I
  17. }. d2 A$ a. y% W) y" X; R; r

  18. 0 Z. t# F% v) I6 L# E# D
  19. void USART2_IRQHandler(void)
    8 B* f% {, b( @
  20. {: N7 G3 C' c, e. n, ]( T+ Z# o. A
  21.     if(USART_GetITStatus(USART2,USART_IT_RXNE)==SET)   //RXNE 标志位为1 表示可以接收数据
    2 V: T8 d. f2 e6 M& o% W
  22.     {3 F1 @, K* j2 ^
  23.         RX_Data=USART_ReceiveData(USART2);& u& e# a; S  K
  24.         Flag=1;; r* \5 z0 P# S! V2 m7 R
  25.         USART_ClearITPendingBit(USART2,USART_IT_RXNE);  //清除RXNE标志位0 v  V3 A/ B/ J1 ]1 a, F
  26.     }# _) P  k' E1 |6 o4 I
  27. }
复制代码
/ 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
微信图片_20230810144705.png 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
收藏 评论0 发布时间:2023-8-10 14:48

举报

0个回答
关于意法半导体
我们是谁
投资者关系
意法半导体可持续发展举措
创新和工艺
招聘信息
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
关注我们
st-img 微信公众号
st-img 手机版