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

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

[复制链接]
攻城狮Melo 发布时间:2023-8-10 14:48
STM32的串口通信

, r; f, ?9 V& Y( n. G
本文在于记录自己的学习过程中遇到的问题和总结,各种情况下串口通信在STM32的实际使用方面占有很大的比重,本文主要对串口通信做一个简要的总结。
# F7 o" v8 ^& S, X' y- X
; H& A" \9 z& y7 t
一、STM32里的串口通信

* l, i3 }* n# S$ b, V4 D+ k4 U
在STM32里,串口通信是USART,STM32可以通过串口和其他设备进行传输并行数据,是全双工,异步时钟控制,设备之间是点对点的传输。
$ |1 G0 a" W% e' d
对应的STM32引脚分别是RX和TX端。STM32的串口资源有USART1、USART2、USART3.
% k8 R* ~$ m* l0 j7 z
串口的几个重要的参数:
6 S: ?" {" j# h( {. B
  • 波特率,串口通信的速率
    7 d2 P4 E& N- b0 k1 z7 q
  • 空闲,一般为高电平

    $ Y) t7 ]3 |0 o! L0 S4 x6 l
  • 起始位,标志一个数据帧的开始,固定为低电平。当数据开始发送时,产生一个下降沿。(空闲–>起始位)

    % g& ^6 h6 z( H7 Q5 ^8 d' K
  • 数据位,发送数据帧,1为高电平,0为低电平。低位先行。

    2 M1 Y9 W/ i6 `# d  w: Y' Z4 i
  • 比如 发送数据帧0x0F 在数据帧里就是低位线性 即 1111 0000

    . U% Y! Q, E  Q) w  C( M
  • 校验位,用于数据验证,根据数据位的计算得来。有奇校验,偶校验和无校验。
    $ f+ N! J0 F* L% M) w/ b
  • 停止位,用于数据的间隔,固定为高电平。数据帧发送完成后,产生一个上升沿。(数据传输–>停止位)
    . d& u. ~$ |  s
6 L* J" I  ?( m7 g
下方就是一个字节数据的传输过程,从图中可以看出,串口发送的数据一般都是以数据帧的形式进行传输,每个数据帧都由起始位,数据位,停止位组成, 且停止位可变。
4 Q* d) q/ X  J; q7 p) O- g
微信图片_20230810144703_3.png + T% K3 I/ t4 P; C

, ^: B0 l2 Z8 {, \4 E0 F8 C

6 w+ `: i- l) k: N7 h
二、串口的发送和接收

% ]5 O7 E; x4 U: e
USART是STM32内部集成的硬件外设,可以根据数据寄存器的一个字节数据自动生成数据帧时序,从TX引脚发送出去,也可以自动接收RX引脚的数据帧时序,拼接成一个字节数据,存放在数据寄存器里。
2 X, \! k7 d# E
当配置好USART的电路之后,直接读取数据寄存器,就可以自动发送数据和接收数据了。在发送和接收的模块有4个重要的寄存器

5 p( S' u, |1 G+ ~
  • 发送数据寄存器TDR
    . s% D  ?5 j- ~5 [7 L# F+ m2 |9 f
  • 发送移位寄存器,把一个字节的数据一位一位的移出去

    8 T% }. K4 s3 ~7 ]8 ?/ D
  • 接收数据寄存器RDR

    8 ~9 \  O+ ~5 A) R7 Y7 {
  • 接收移位寄存器,把一个字节的数据

    . c: X: j7 h2 s: n
4 _8 l5 u; G; [' V  V) b8 G$ y
下方为串口的发送和接收图解:

9 i9 @5 P1 v" V* U/ e  m( m2 J0 [! Q
微信图片_20230810144703_2.png
# M8 O2 G: }. u0 ?$ K6 G
. {) `9 i0 Y  G% m! O' f
串口发送
8 n8 @6 \1 }* B  k' {% X$ w
在配置串口的各个参数时,可以选择发送数据帧的数据位的大小,可选8位或9位。

- e5 O; q% Q$ N4 m/ B
串口发送数据实际上就是对发送数据寄存器TDR进行写操作。
% m4 a' h. O+ x$ X8 Z- o3 u2 H9 A' O2 d
1. 当串口发送数据时,会检测发送移位寄存器是不是有数据正在移位,如果没有移位,那么这个数据就会立刻转移到发送移位寄存器里。准备发送。

! C7 Z) D  N, I
2. 当数据移动到移位寄存器时,会产生一个TXE发送寄存器空标志位,该位描述如下。当TXE被置1,那么就可以在TDR写入下一个数据了。即发送下一个数据。
. e1 q0 Y' B6 o7 y7 i' h7 g
微信图片_20230810144703_1.png
' |6 K" G# m: a; t
+ |5 U6 l# P# J2 U
3. 发送移位寄存器在发送器控制的控制下,向右移位,一位一位的把数据传输到TX引脚。

$ [, A) F$ t& R9 n1 y
微信图片_20230810144703.png 6 {0 @7 g' O. }. M
2 n* h; p+ [' N, V7 y
4. 数据移位完成后,新的数据就会再次从TDR转移到发送移位寄存器里来,依次重复1-3的过程。通过读取TXE标志位来判断是否发送下一个数据。

) n  U, E5 E% T1 b7 k0 {7 j! F! t7 J
串口接收

7 S% T# a( h1 t
数据从RX引脚通向接收移位寄存器,在接收控制的控制下,一位一位的读取RX的电平,把第一位放在最高位,然后右移,移位八次之后就可以接收一个字节了。
/ U7 @+ T3 u4 j
当一个字节数据移位完成之后,这一个字节的数据就会整体的移到接收数据寄存器RDR里来。

$ `* F6 S, A1 l+ d: n  _$ I" ^9 p
在转移时会置RXNE接收标志位,即RDR寄存器非空,下方为该位的描述。当被置1后,就说明数据可以被读出。
4 u8 p- e& w0 _1 F* S5 Z2 x$ z- o( K
微信图片_20230810144704_3.png 0 `" S# d; C6 ^+ w) L' |/ p2 [/ l
* p0 c8 F: }2 ]2 Z5 U9 m  Q
下图即为串口接收的工作流程
. n$ {* d% v3 v, _
微信图片_20230810144704_2.png
0 B1 i: ], i# q+ K' s4 {! }2 i. N# }! D- P( S6 i) ?, }
1 k$ s' d- l, }5 n' Q
三、串口在STM32中的配置
& e* V4 |( ?6 O  _1 c, ^
首先要明确几点:使用STM32串口外设中的哪一个?串口发送或者接收数据?串口相关的参数配置?发送或接收是否使用到中断?
9 L  h1 y5 W- N3 g: d0 q4 Y
下方为串口发送的配置。

- Z+ t/ _4 d, q5 j1 _& ]
1. RCC开启USART、串口TX/RX所对应的GPIO口
  1. RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2,ENABLE);  //开启USART2的时钟* N. G. z4 {9 S1 G2 x1 g4 x
  2. RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);   //开启GPIOA的时钟
复制代码

" X" M6 \8 i" [2 n* v5 y
2. 初始化GPIO口
0 W; `/ J9 {1 d( A/ Y- C
这里注意哈,根据自己的需求来配置GPIO口,发送和接收是都需要还是只需要其中一个。然后对应的根据引脚定义表来初始化对应的GPIO口。

) b6 A( V; M" U  D
USART2对应的引脚

" s7 g- B/ y5 M" j* Q, {
微信图片_20230810144704_1.png % e) ?' C. \- ~, R+ j' r* ]

- [( B: O6 L3 [( J" a5 D) F& o
USART1对应的引脚

0 J( d( P5 k, H5 l- \" K
微信图片_20230810144704.png
+ w4 u  n9 ]4 s: O; Q+ h% d
& C$ k, U6 t, s8 ]
这里根据手册来看,RX引脚模式配置成浮空输入或者上拉输入。TX引脚模式配置成复用推挽输出。
8 a. F7 Y, S' s
**比如我这里只初始化TX发送端**
  1. //TX端
    # x, F, f$ S$ `% i- P( g
  2.     GPIO_InitTypeDef GPIO_InitStructure;4 `; `* k0 D- W7 \' J
  3.     GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_PP;  //复用推挽输出! g) M6 U) C: _& e/ K
  4.     GPIO_InitStructure.GPIO_Pin=GPIO_Pin_2;   //USART2对应的TX端为GPIOA2
    / k9 p( q" Y5 n& e3 Z( l# O8 A
  5.     GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;  //50MhZ
    ! k% ^7 J; _; v+ Q& B5 R; V; N
  6.     GPIO_Init(GPIOA,&GPIO_InitStructure);
复制代码
0 E( m9 c+ r" N1 |% j# T
3. 串口初始化
) o3 s9 M+ f- p/ ~. |
注意哈,USART_Init()这个函数,是用来配置串口的相关参数的。
+ i! A* {: z+ M3 v3 D0 w. R% r
  • USART_BaudRate 串口通信使用的波特率 一般是9600或者是115200,这里我们给9600

    ; U9 H- B6 r) F  I6 {6 q
  • USART_HardwareFlowControl 是否选择硬件流触发,一般这个我们也不选,所以选择无硬件流触发。
    8 l* d5 V' R( U
  • USART_Mode 这个参数要注意了哈,串口的模式,发送模式还是接收模式,还是两者都有
    + K6 K6 _6 Z" x- ]1 s: X% Z( ^# p* |' P
  • USART_Parity 校验位,可以选择奇偶校验和不校验。没有需求就直接无校验

    - u5 }( z* ^5 W3 I4 j
  • USART_StopBits 停止位 有1、0.5、2位,我们这里选1位停止位
    2 s/ I3 D  i$ m# O
  • USART_WordLength 数据位 有8位和9位可以选择
    1. //串口初始化
      4 o% N# c6 `- r
    2.   USART_InitTypeDef USART_InitStruct;/ d. J- L/ n/ i, _7 y
    3.   USART_StructInit(&USART_InitStruct);  //初始默认值; S4 M" D$ n. a2 Q
    4.   USART_InitStruct.USART_BaudRate=9600;9 ~+ q, Z& u, E/ |8 {
    5.   USART_InitStruct.USART_HardwareFlowControl=USART_HardwareFlowControl_None;   //不使用硬件流触发
      & i$ @% n# P* N2 q
    6.   USART_InitStruct.USART_Mode=USART_Mode_Tx;         //TX 发送模式
      5 [  [4 @8 ?- Y  l6 E2 v
    7.   USART_InitStruct.USART_Parity=USART_Parity_No;       //不选择校验) v# i. [  m* ?+ f" Y
    8.   USART_InitStruct.USART_StopBits=USART_StopBits_1;      //停止位1位
      + ~$ u3 y- g: U# P6 V( ]
    9.   USART_InitStruct.USART_WordLength=USART_WordLength_8b;  //数据位8位0 s/ N! f+ n8 b1 e+ M2 Z0 |( {% J
    10.   USART_Init(USART2,&USART_InitStruct);
    复制代码

    . s' [8 a; X2 J. n& g8 q

" A" D9 o4 X8 F1 z9 a
4. 串口使能
  1. //串口使能
    8 ~( h  {* ]/ u6 E$ @
  2.    USART_Cmd(USART2,ENABLE);
复制代码

. q% D4 F" D6 Q% ^
5. 串口发送数据

( Z1 _/ u5 z* j注意哈,我们要判断TXE标志位的状态。0,数据还没有被转移到移位寄存器;1,数据已经被转移到移位寄存器。
+ @  k+ i; f: _5 e2 }4 N- S
当TXE标志位为1时,就说明可以发送下一个数据了。详细过程可看上面串口发送的解释。
  1. void Serial_SendByte(uint16_t Byte)
    7 u8 Q. k1 W& \% _( e2 Y
  2. {2 k' o  X$ W# s5 o6 Q+ j( g+ `
  3.   USART_SendData(USART2,Byte);
    2 z$ j- b" V: o# j3 M3 Z& G) c
  4.   //0 表示数据还未转移到移位寄存器 循环等待 1 数据已经被转移到了移位寄存器可以发送数据
    , ]+ m7 L4 a; t1 Y: O* _+ V% z; M
  5.   while(USART_GetFlagStatus(USART2,USART_FLAG_TXE)==RESET);  //不需要手动清零 再次写入TDR时会自动清零  ; t1 [" H& o* N, L/ ~
  6. }
复制代码
" v6 w8 d  a! J
经过上述五步的配置,单片机就可以通过串口发送数据了。

0 }7 v" P) c9 g8 b  C- C
下方为发送数据图例,STM32向串口发送0x16数据

- W7 h' r* E& M2 B. b
微信图片_20230810144705_3.png $ P  I1 e% d5 _

2 B6 K1 w9 Y; x0 K3 {% G: O+ h- y- A
四、串口接收的两种实现方式
& P8 O) L* w9 x  s9 a
上方是发送数据的例子,那么串口接收又该如何配置,又要在串口发送的例子上做哪些更改呢?

. Q0 q( }$ w: `% k) F( D
这里我们可以通过查询或者中断的方式来进行接收数据的两种方式。
; @' W6 Z, ~4 y, {
  • 查询方式就是通过不断的查询RXNE标志位,通过判断RXNE位的状态来确定数据是否接收。

    : f) _- W& K9 ]+ l: D' ?; P) F% N
  • 中断方式就是通过配置接收输出控制通道,配置NVIC,在中断服务子函数里进行数据的接收。
    8 R& p8 }0 w! h+ N) L
' Q' Y/ X0 S$ P$ u' k+ S
1. 需要更改的地方! `3 S. n1 I0 J( R& L: l7 ]
8 N0 V+ I, ]: I+ @
既然我们要实现串口的接收,那么就要配置串口RX引脚,在串口模式中添加USART_Mode_RX模式。

  @; ?' w( w1 ]8 b& j, r1 C; r
  • 初始化RX引脚
    1. //RX端/ Q6 {& l7 G- ?% \" r1 J
    2.   GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IPU;  //上拉输入" \6 j( H, o: n: d; X
    3.     GPIO_InitStructure.GPIO_Pin=GPIO_Pin_3;8 x- e3 B; c8 A; b" q
    4.     GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;  //50MhZ, M- \( I! Q; b* b! W  N+ \$ E
    5.     GPIO_Init(GPIOA,&GPIO_InitStructure);
    复制代码
    - @  x( c! ~: ]9 P3 k
. k& _4 g  X. O0 p
  • 添加串口模式
    1. USART_InitStruct.USART_Mode=USART_Mode_Tx|USART_Mode_Rx;         //TX 发送模式  RX 接收模式
    复制代码
    - G4 m6 z$ j" C& h, l, V4 F

* g/ S' J7 O* a7 \2 N* ?9 A
2. 查询RXNE标志位

9 B& B' s( Y; h. ?( i
这里我们还是来看一看RXNE标志位的描述
" i7 s8 X# C% G0 j8 @
微信图片_20230810144705_2.png ) K$ e+ v8 z6 I& v

; V% c- c; U1 F! a
上图描述,为0时数据没有收到,为1时收到了数据,数据可以从RDR里读出
# Z9 L/ d) h. x$ h3 h
所以在主程序里不断读取RXNE标志位,如果为1,表示数据可以读出
  1. uint8_t RX_Data;
    ) {$ h# d! c, B( G4 c) A
  2. int main()
    % q' }& k3 o0 A1 E
  3. { ! k" l- q9 [6 `: w
  4.     Serial_Init();+ W9 y0 s! |* `) \& O
  5.     Serial_SendByte(0x16);
    2 O+ N6 @/ J- W" I( D9 E
  6.     while(1)
    2 ~, B8 Q/ @+ @% O
  7.     {
    " G6 F# J. w8 }9 U% F( w
  8.         if(USART_GetFlagStatus(USART2,USART_FLAG_RXNE)==SET)   //0 循环等待 1 可以接收数据
    7 Y6 I9 @5 ?4 z% ]2 W
  9.         {0 N0 o* {- K* Z9 ]6 ^# H' Y5 u1 o
  10.             RX_Data=USART_ReceiveData(USART2);           
    3 `8 J# a& ^/ S- V0 S
  11.       Serial_SendByte(RX_Data);4 M2 u9 f, O$ o0 M' X
  12.         }. I" |$ d5 Z- p9 n
  13.     }
      h' f; D9 }+ u4 ]' \
  14. }
复制代码

  H2 K( P, x/ a* y, m0 L4 `- o
下图为程序现象:pc向单片机发送数据0x15,单片机接收数据0x15,并且把接收到的数据作为数据发送到pc,在pc上显示0x15。
1 U; I/ v! j/ R5 E* h9 H) [
微信图片_20230810144705_1.png 9 c6 V' Z" w% w, Q2 X

& T& v* r, m$ o0 a* _- r6 A' c
3. 使用中断
# F# T& m, b* g. C, p
  • 通过配置串口的接收作为中断源,开启中断输出控制,配置NVIC。开启中断通道。
    ' R) q2 ~8 n: H; _7 n
  1. //开启中断输出控制% }* S+ Y2 d7 A! ]' |
  2.   USART_ITConfig(USART2,USART_IT_RXNE,ENABLE);
    " {5 y+ ^6 h8 X
  3.   
    - E$ ~' G9 [7 U. C
  4.   //配置NVIC
    " O2 m  ]6 U0 F# P+ v- O6 y
  5.   NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    + {' Z4 T8 t+ h4 X/ `. g
  6.   NVIC_InitTypeDef NVIC_InitStruct;
    ' y4 p& L9 n2 N# O& {
  7.   NVIC_InitStruct.NVIC_IRQChannel=USART2_IRQn;   //选择USART2的中断通道8 q# _; F0 q4 W
  8.   NVIC_InitStruct.NVIC_IRQChannelCmd=ENABLE;    //中断使能
    , i5 ~7 Z& p; d7 ^6 Z! p
  9.   NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority=1;+ J  @( n( j1 T3 D+ y5 [# d" v
  10.   NVIC_InitStruct.NVIC_IRQChannelSubPriority=1;
    . ]+ l, k% ^, g- P: S, D! d* P, w+ _; Z
  11.   NVIC_Init(&NVIC_InitStruct);
复制代码
3 q5 O# u3 ^- a/ W
  • 中断服务子函数
    ; L# t( t8 l$ `& {$ Q
2 h; N( I) H, A; L5 X! H/ A
中断服务子函数写好后,就可以在中断里读取接收到的数据了。

% E) C, Q# S( t" Q. `当接收到数据后,触发接收中断,主程序暂停执行。接收完数据后主程序回复执行。当接收到数据时,就触发中断。
  1. void USART2_IRQHandler(void)
    + R2 s; W, c; T- ?4 @# H0 _
  2. {# F2 S0 Q5 T% |, x
  3.   if(USART_GetITStatus(USART2,USART_IT_RXNE)==SET)   //RXNE 标志位为1 表示可以接收数据" h( k3 X6 d* A6 ~
  4.   {
    4 t. p3 x5 N; y  W; |! u
  5.     RX_Data=USART_ReceiveData(USART2);
    6 G; ^4 j: s8 B$ Z! d. c- G
  6.     Flag=1;/ n% [3 ]7 P% ~( w9 O4 M% G
  7.     USART_ClearITPendingBit(USART2,USART_IT_RXNE);  //清除RXNE标志位0 J: K% B; @( o8 ~* e
  8.   }6 ~+ R( o- O9 v+ k
  9. }
复制代码

- {- i% M* x7 n6 B
  • 主程序测试
    $ ^: _* k  {& f* M1 ~: K. C
  1. uint8_t RX_Data;
    7 i" t/ h- x$ K! {5 ]& Y
  2. uint8_t Flag;
    ) ]( h" |. \+ u9 t5 x; ?5 J+ A

  3. ; b  v/ I5 Z; i
  4. int main()
    ; P" Z# l2 d5 D/ f
  5. {
    4 \/ A* `) ?  A$ `, G- q6 s
  6.     Serial_Init();3 d) l$ A- O  s
  7.     Serial_SendByte(0x16);
    9 @6 Z- Z# }3 u" w  c/ \, Z* ]% |- ?) F
  8. 7 H% j/ v  U" a2 A. @8 S

  9. : i5 [9 K7 e! Y+ L3 L& H7 \  O
  10.     while(1); w5 K1 e4 r5 I7 y: Y
  11.     {
    # N% l8 z- \; O" p) I
  12.         if(Flag==1)  X* P! w0 }6 e+ U
  13.         {
    9 ~/ y3 `9 T0 x' q, _
  14.             Serial_SendByte(RX_Data);
    " `2 R" }+ D4 M
  15.         }) a+ k! O$ p. \7 T. K8 K
  16.     }
    7 W; f" {$ W# p0 R% r! p9 S; T! i
  17. }
    - d, c3 g2 _  Q# ^& J- R$ l8 B% t

  18. # }: P4 r! W' x9 v% z8 K) d2 a
  19. void USART2_IRQHandler(void)
    8 o% ^( |/ r' G$ M. j" `* V  Z
  20. {
    : _' s' D2 b" F! l8 V3 K: W* }
  21.     if(USART_GetITStatus(USART2,USART_IT_RXNE)==SET)   //RXNE 标志位为1 表示可以接收数据" g  S0 v3 f1 ~, _
  22.     {1 N2 U( Y" f6 K8 J4 s
  23.         RX_Data=USART_ReceiveData(USART2);" X8 Z" k; P% ?$ U! B" y
  24.         Flag=1;
    + \8 f$ j8 M4 W8 D4 ~/ z
  25.         USART_ClearITPendingBit(USART2,USART_IT_RXNE);  //清除RXNE标志位
    ( C- ?' V2 m1 D
  26.     }
    4 {3 X4 H+ h. O0 @; U
  27. }
复制代码

$ E- \: i* L) B& r0 r5 A

/ ]! C7 \/ g* h. n
下图为程序现象:可以看到,串口确实收到了数据,只是我把接收到的数据0xFE放在了while循环里,这说明数据接收是成功的,使用中断是可行的。

) T& a( K& ~/ k& v. D( q" [, |
微信图片_20230810144705.png + l0 ^1 E: r5 J# p

3 e1 B4 {5 T( w7 v

0 A: T* G7 c2 i/ U( ~. o" W. y' ]
总结
& O, X: C3 j+ P- k8 h; Z) K4 H
到这里,就大致总结了串口的发送和接收。
+ |. R0 P4 N) \: b
串口的配置,使用查询或者中断来接收数据。
5 ~  I8 E+ r2 |- B' [
串口的使用会很常用到,所以在这里对串口做一个总结,也算是对之前知识的一个回顾和总结,加强印象。

5 X  Q3 j. J' S! U$ @4 j, e
转载自: 古月居
如有侵权请联系删除

3 v' k: t& J* ?- Q5 ~; N' [. x
& ^$ P/ f8 L* B
赞 收藏 评论0 发布时间:2023-8-10 14:48

举报

0个回答

所属标签

相似技术帖

关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
人形机器人运动控制、感知与智能配电
半导体创新技术与应用方向
EE架构与软件定义汽车
12V/48V 汽车智能配电(SPD)
区域控制单元(ZCU)与分区架构
关注我们
st-img 微信公众号
st-img 手机版