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

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

[复制链接]
攻城狮Melo 发布时间:2023-8-10 14:48
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
微信图片_20230810144703_3.png , h" v) \- |( k4 t7 {

* X  E2 ?* ], ~
( ?$ c% x7 e7 V% c' R. E
二、串口的发送和接收

7 y" a5 s9 z1 Z( V1 B" o0 e
USART是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
微信图片_20230810144703_2.png ) 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
微信图片_20230810144703_1.png
" u5 A# j- S/ q+ Q6 ~
  h4 \& n! z% _; p* O6 y
3. 发送移位寄存器在发送器控制的控制下,向右移位,一位一位的把数据传输到TX引脚。
- N4 ?  L. S$ N% }% B
微信图片_20230810144703.png
, 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
微信图片_20230810144704_3.png
5 u9 X3 W- L/ W, m( L- }* f4 D$ ^  k

+ }) J8 S- x* e, p( f
下图即为串口接收的工作流程

* }* {* e! h( V& O5 e' P- v
微信图片_20230810144704_2.png 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口
  1. RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2,ENABLE);  //开启USART2的时钟# U7 h# @8 S. G5 |" s# w
  2. 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
微信图片_20230810144704_1.png
5 z3 `" j. R$ s2 O

9 b4 k$ Y8 o$ Y& u& G) n* C
USART1对应的引脚

. v4 q5 @" F6 |$ Z9 G% b
微信图片_20230810144704.png 7 x8 r2 D' v& @

$ }( y$ d: J8 I  z2 c! P6 Q# W
这里根据手册来看,RX引脚模式配置成浮空输入或者上拉输入。TX引脚模式配置成复用推挽输出。
, n) U4 Y( J+ R4 j# c
**比如我这里只初始化TX发送端**
  1. //TX端
    0 R* Y- [5 D3 r% j+ ^+ e% _6 N+ F
  2.     GPIO_InitTypeDef GPIO_InitStructure;
    % ]& D5 j1 O2 P! P, E
  3.     GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_PP;  //复用推挽输出
    . V, U- ^7 d9 Y2 ]
  4.     GPIO_InitStructure.GPIO_Pin=GPIO_Pin_2;   //USART2对应的TX端为GPIOA2
      \2 c2 r$ u7 c* `6 H( I
  5.     GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;  //50MhZ
    2 N  z: a  m: i
  6.     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位可以选择
    1. //串口初始化
      # w7 V7 A+ E$ A" i" ?) t
    2.   USART_InitTypeDef USART_InitStruct;% P  m$ z& R$ w! Y$ Z  P& W4 ^0 p
    3.   USART_StructInit(&USART_InitStruct);  //初始默认值
      5 i) Z5 o: `0 t: w4 c
    4.   USART_InitStruct.USART_BaudRate=9600;! E/ M- Z! U( T% _5 w
    5.   USART_InitStruct.USART_HardwareFlowControl=USART_HardwareFlowControl_None;   //不使用硬件流触发) o0 E+ `' N& x+ g' T3 w
    6.   USART_InitStruct.USART_Mode=USART_Mode_Tx;         //TX 发送模式  v" d: b7 \- }
    7.   USART_InitStruct.USART_Parity=USART_Parity_No;       //不选择校验; S' O, I0 G9 W& c- |: `
    8.   USART_InitStruct.USART_StopBits=USART_StopBits_1;      //停止位1位9 O( R9 e. F$ v  \* }
    9.   USART_InitStruct.USART_WordLength=USART_WordLength_8b;  //数据位8位
      , A# f7 v' |/ m/ n) s4 s
    10.   USART_Init(USART2,&USART_InitStruct);
    复制代码

    6 q2 p. C& _6 o; F8 E0 I5 N) |

/ X' p' {/ g8 Z) Q
4. 串口使能
  1. //串口使能  W& x* a& Q7 m
  2.    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时,就说明可以发送下一个数据了。详细过程可看上面串口发送的解释。
  1. void Serial_SendByte(uint16_t Byte)$ }' L3 `  J. K4 y5 s( [7 |& M
  2. {
    % G4 \" P; p# _8 X
  3.   USART_SendData(USART2,Byte);8 ?3 A3 ?  q2 ?0 B& X1 a
  4.   //0 表示数据还未转移到移位寄存器 循环等待 1 数据已经被转移到了移位寄存器可以发送数据
    ) A  [3 R( h6 }. R- p
  5.   while(USART_GetFlagStatus(USART2,USART_FLAG_TXE)==RESET);  //不需要手动清零 再次写入TDR时会自动清零  6 Z" u0 X9 o8 @' }: I. }
  6. }
复制代码

/ D$ Y7 Q/ f3 z, O
经过上述五步的配置,单片机就可以通过串口发送数据了。

' o  z* N$ ]' ^
下方为发送数据图例,STM32向串口发送0x16数据
( u, g% P- j# k/ a; x
微信图片_20230810144705_3.png 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
  • 查询方式就是通过不断的查询RXNE标志位,通过判断RXNE位的状态来确定数据是否接收。
    + T& Q( k, q7 D7 J$ z
  • 中断方式就是通过配置接收输出控制通道,配置NVIC,在中断服务子函数里进行数据的接收。

    / z' L$ H) S9 ^% o3 U( H! y5 F4 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引脚
    1. //RX端; g; l+ e! C) C5 `; B
    2.   GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IPU;  //上拉输入  j0 K7 g" z" l' l6 u
    3.     GPIO_InitStructure.GPIO_Pin=GPIO_Pin_3;- p/ g" K$ ]0 h5 g. m
    4.     GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;  //50MhZ
      5 T. v* n" k# }% w
    5.     GPIO_Init(GPIOA,&GPIO_InitStructure);
    复制代码
    ) n$ c) Z1 s2 M7 l& s

' d! [: a3 P7 ]4 O7 V
  • 添加串口模式
    1. 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
微信图片_20230810144705_2.png ' j# @  q; ]1 {0 [

& A7 {: B# _( E& I8 ?7 g
上图描述,为0时数据没有收到,为1时收到了数据,数据可以从RDR里读出

: Y: H" e7 |. L; C5 ~
所以在主程序里不断读取RXNE标志位,如果为1,表示数据可以读出
  1. uint8_t RX_Data;- E: V+ y  r, }2 v$ J
  2. int main()
    ( |) G5 [- r' S2 `1 N( W
  3. {
    ' @6 Y+ I0 X: e. e# b/ w5 t7 z
  4.     Serial_Init();$ G' _' o- g. N# J9 m% S) J* Y
  5.     Serial_SendByte(0x16);" ~: _  y! J4 _% z! ^5 W+ l  o
  6.     while(1)3 H: H5 C6 ~) L3 D
  7.     {9 v  |; h8 p8 z  E
  8.         if(USART_GetFlagStatus(USART2,USART_FLAG_RXNE)==SET)   //0 循环等待 1 可以接收数据7 T, I" I6 w# C2 y( z) M7 i% M
  9.         {7 g2 d7 |% O; v
  10.             RX_Data=USART_ReceiveData(USART2);           $ Z6 r9 b0 R1 Z. a8 M$ K; |
  11.       Serial_SendByte(RX_Data);+ b; ]7 Y5 ^9 D' r
  12.         }
    % q% C0 X9 {) ]. a8 N
  13.     }
    $ |1 e2 Q; _- z& ]( U7 `- }
  14. }
复制代码
! u3 R# x* N4 Y, E
下图为程序现象:pc向单片机发送数据0x15,单片机接收数据0x15,并且把接收到的数据作为数据发送到pc,在pc上显示0x15。

7 L5 E8 @* R1 {" D( K( ]
微信图片_20230810144705_1.png , [* M3 P! t; f  g+ ~
1 H4 N2 Y2 P( T/ e2 V
3. 使用中断
6 G) T/ x% r: ~4 {- H! |- k
  • 通过配置串口的接收作为中断源,开启中断输出控制,配置NVIC。开启中断通道。

    % w- j+ `8 c, X0 v; S6 F2 g, ~8 o
  1. //开启中断输出控制
    % ]1 s! @* `# _9 Y
  2.   USART_ITConfig(USART2,USART_IT_RXNE,ENABLE);: |. S! q& Z* G1 j
  3.   
    ! W, X  u; ]8 o+ q% s6 _! f
  4.   //配置NVIC& m" `) Y: w- D  D9 T, U
  5.   NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); - J1 ~! \/ e0 |% r2 @! N, a
  6.   NVIC_InitTypeDef NVIC_InitStruct;
    2 t  Y' z% M6 {$ }  O
  7.   NVIC_InitStruct.NVIC_IRQChannel=USART2_IRQn;   //选择USART2的中断通道
    $ c( t  o+ A5 W; N8 N5 y2 t7 Y
  8.   NVIC_InitStruct.NVIC_IRQChannelCmd=ENABLE;    //中断使能
    9 T5 G8 E8 r2 U. Y6 r; o
  9.   NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority=1;
    $ Z# T( r2 H$ A$ V
  10.   NVIC_InitStruct.NVIC_IRQChannelSubPriority=1;: p9 A; x* v& i6 T- [7 d0 c; {) k4 X
  11.   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: @
当接收到数据后,触发接收中断,主程序暂停执行。接收完数据后主程序回复执行。当接收到数据时,就触发中断。
  1. void USART2_IRQHandler(void)
    . p7 i1 u( m& w7 P: ]- s
  2. {
    : G7 M% T/ J$ |' J) T" |
  3.   if(USART_GetITStatus(USART2,USART_IT_RXNE)==SET)   //RXNE 标志位为1 表示可以接收数据4 c  Q. b7 h7 I
  4.   {
    2 o: {! F0 E/ n1 C
  5.     RX_Data=USART_ReceiveData(USART2);
    & |0 N7 @/ O( ^
  6.     Flag=1;, s) {6 \/ o  `$ O
  7.     USART_ClearITPendingBit(USART2,USART_IT_RXNE);  //清除RXNE标志位
    8 R) k* i$ q4 h  N' s
  8.   }
    % j' [" Z' W8 S, T. A7 J
  9. }
复制代码

- ^; z3 W; @9 _( H4 G$ s1 r
  • 主程序测试

    ) f* k) t$ P: x& o  v, ~( A9 T  R1 c
  1. uint8_t RX_Data;; S) _* ^4 f/ e5 S
  2. uint8_t Flag;% J+ A; E8 G* g. Y0 P

  3. 7 I+ y& A# k. w- |8 X& X6 P( ]
  4. int main()
      x: x: g% J. W$ X
  5. {
    6 }1 c5 y! N4 [6 w* y
  6.     Serial_Init();
    # [5 Q6 E4 J7 f5 B
  7.     Serial_SendByte(0x16);& @  b3 S2 }9 T  r4 |+ n
  8. ) g- z" a4 y0 k4 t. g

  9. 3 L9 E; V0 T% {
  10.     while(1)
    2 c7 }+ u/ |3 l$ l5 X
  11.     {3 E5 L( R; a! o! j) G+ ^
  12.         if(Flag==1)! |0 V6 m7 o+ T
  13.         {, F7 Y$ g: t; \0 D
  14.             Serial_SendByte(RX_Data);
    7 Y5 g1 x/ K% d: f3 q7 B9 b
  15.         }
    1 R/ Q# ^- n- M- n
  16.     }
    ! o: R9 I) ~, q5 S+ D4 f
  17. }
      T& w$ s/ v4 ~8 V% {
  18. 6 k; s1 W  ?, D1 F3 j3 T- e& {% i
  19. void USART2_IRQHandler(void)
    * k! I2 X+ \; n
  20. {
    7 {, g4 J1 c; x$ a$ M; a
  21.     if(USART_GetITStatus(USART2,USART_IT_RXNE)==SET)   //RXNE 标志位为1 表示可以接收数据4 t: Q( h& o3 Q' [) n! }
  22.     {! x) n9 s, K# k
  23.         RX_Data=USART_ReceiveData(USART2);
      k% y1 c; f3 \- ?" _
  24.         Flag=1;4 S2 C- T$ i' g- o* p8 {: p' d% B7 y+ j
  25.         USART_ClearITPendingBit(USART2,USART_IT_RXNE);  //清除RXNE标志位
    ' D3 b! a2 m) R  w. L. k9 t/ X
  26.     }- o* W: B2 {* C' E5 G: E; Y! R3 {
  27. }
复制代码

* Q4 u  _  @* ^. a' J3 \: b
4 [  d# Q* x# ]2 s' }- m. p
下图为程序现象:可以看到,串口确实收到了数据,只是我把接收到的数据0xFE放在了while循环里,这说明数据接收是成功的,使用中断是可行的。
7 a" g$ P0 {3 @7 g2 \5 \# n
微信图片_20230810144705.png
! 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
收藏 评论0 发布时间:2023-8-10 14:48

举报

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