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

基于STM32串口通信经验分享

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

! h) N+ \9 R0 G, s1 x) b
本文在于记录自己的学习过程中遇到的问题和总结,各种情况下串口通信在STM32的实际使用方面占有很大的比重,本文主要对串口通信做一个简要的总结。
. \% w2 ~; a' \
一、STM32里的串口通信
在STM32里,串口通信是USART,STM32可以通过串口和其他设备进行传输并行数据,是全双工,异步时钟控制,设备之间是点对点的传输。

  a( F  `" |9 K+ Y
对应的STM32引脚分别是RX和TX端。STM32的串口资源有USART1、USART2、USART3.
! C1 f3 e* z, s, u% G5 k
串口的几个重要的参数:
; D% u+ _! K% R$ E5 a* G5 p
  • 波特率,串口通信的速率

    % z- z7 [7 G3 f% x
  • 空闲,一般为高电平
    4 j- L1 K4 ^) u; `0 Y; v8 F% Z- ~
  • 起始位,标志一个数据帧的开始,固定为低电平。当数据开始发送时,产生一个下降沿。(空闲–>起始位)

    1 \* k9 p1 T  |; C' S% Y3 V
  • 数据位,发送数据帧,1为高电平,0为低电平。低位先行。

    & h- H; r0 Y* \
  • 比如 发送数据帧0x0F 在数据帧里就是低位线性 即 1111 0000
    % ^, B( ^9 N/ d1 \
  • 校验位,用于数据验证,根据数据位的计算得来。有奇校验,偶校验和无校验。
    . J4 n1 D! y0 Y( Y
  • 停止位,用于数据的间隔,固定为高电平。数据帧发送完成后,产生一个上升沿。(数据传输–>停止位)

    ! A; g4 Y0 _2 k. z! X
% M9 `8 h8 h' o1 p( Z+ e
下方就是一个字节数据的传输过程,从图中可以看出,串口发送的数据一般都是以数据帧的形式进行传输,每个数据帧都由起始位,数据位,停止位组成, 且停止位可变。
7 l& C% |& p) H( g9 M) m  {
微信图片_20230726144152_2.png 3 Z; p8 c6 P* z; v: V1 H$ H+ A1 z# C) {

9 [, {1 u2 a; y; m9 H( |

/ U4 a0 H" d; C8 i9 U
二、串口的发送和接收
USART是STM32内部集成的硬件外设,可以根据数据寄存器的一个字节数据自动生成数据帧时序,从TX引脚发送出去,也可以自动接收RX引脚的数据帧时序,拼接成一个字节数据,存放在数据寄存器里。
/ S5 m3 l% x  w' B  P" s5 F4 @
当配置好USART的电路之后,直接读取数据寄存器,就可以自动发送数据和接收数据了。在发送和接收的模块有4个重要的寄存器

* P- v7 B; }( ?) ]  V! w
  • 发送数据寄存器TDR
    % T) L! j9 f" }
  • 发送移位寄存器,把一个字节的数据一位一位的移出去

    ) N, k- @. X$ x
  • 接收数据寄存器RDR

    ( {6 t6 U3 Z% b' M
  • 接收移位寄存器,把一个字节的数据

    ! s6 Q; z, M4 P
  x+ r( L/ A' f( O- ]
下方为串口的发送和接收图解:
- F% X, g) i  x$ @" N, x
微信图片_20230726144152_1.png
+ s. L9 U: {8 e/ q2 x

; R, L4 k! j' @9 X
串口发送

( R* o1 u6 H$ N3 J- F. m' |' a; F  R" X
在配置串口的各个参数时,可以选择发送数据帧的数据位的大小,可选8位或9位。
( f( _" Z) _, Z- K- D( E) y
串口发送数据实际上就是对发送数据寄存器TDR进行写操作。

) o: N% U, A: k+ q
1. 当串口发送数据时,会检测发送移位寄存器是不是有数据正在移位,如果没有移位,那么这个数据就会立刻转移到发送移位寄存器里。准备发送。
, k- j/ j& S; y
2. 当数据移动到移位寄存器时,会产生一个TXE发送寄存器空标志位,该位描述如下。当TXE被置1,那么就可以在TDR写入下一个数据了。即发送下一个数据。

$ b' ^+ a) T/ z* X& [3 w0 F
微信图片_20230726144152.png & d, t6 f( e- a% |  w2 W

0 y+ F0 \8 r9 e( J1 R9 d
3. 发送移位寄存器在发送器控制的控制下,向右移位,一位一位的把数据传输到TX引脚。
/ J6 R4 J. w( ]
微信图片_20230726144153_3.png
; y+ W% _1 y7 p6 U& E& j5 ~
9 I& r) |" |9 a0 q- @  Q
4. 数据移位完成后,新的数据就会再次从TDR转移到发送移位寄存器里来,依次重复1-3的过程。通过读取TXE标志位来判断是否发送下一个数据。

! j+ Z6 ^0 W% q+ Q
串口接收

5 L. E( s+ `. {) D9 C% A* @
数据从RX引脚通向接收移位寄存器,在接收控制的控制下,一位一位的读取RX的电平,把第一位放在最高位,然后右移,移位八次之后就可以接收一个字节了。

) S* C% m* z( D  R
当一个字节数据移位完成之后,这一个字节的数据就会整体的移到接收数据寄存器RDR里来。
5 T( {4 A4 L* {+ i* W" O9 D! J
在转移时会置RXNE接收标志位,即RDR寄存器非空,下方为该位的描述。当被置1后,就说明数据可以被读出。

" _9 Z8 \; P% r
微信图片_20230726144153_2.png
* i0 T& @6 Z% q6 g  D1 k

8 y; M% T; Z+ Z7 ~' l- e
下图即为串口接收的工作流程

) Q7 F! d/ Q9 k+ N, \; A( t6 O
微信图片_20230726144153_1.png ( b! n% C# c; l8 q% P( U

; h# @* z6 t& P$ ]/ D6 U0 R
三、串口在STM32中的配置
8 [( G5 ?$ y* z, l3 l% Q
首先要明确几点:使用STM32串口外设中的哪一个?串口发送或者接收数据?串口相关的参数配置?发送或接收是否使用到中断?

$ O3 a( o9 |* I5 S5 w* B& r
下方为串口发送的配置。
: u- g! c2 O  B- ]/ e
1. RCC开启USART、串口TX/RX所对应的GPIO口
- @5 p# ^8 F  p7 s3 t' U: V* Z
  1. RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2,ENABLE);  //开启USART2的时钟% s$ w" Y6 Q: X8 c  ?$ i
  2. RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);   //开启GPIOA的时钟
复制代码

$ K( Q( h) s5 `5 }( Q
2. 初始化GPIO口

3 J9 F, J; O/ o5 a8 h* }/ O" G
这里注意哈,根据自己的需求来配置GPIO口,发送和接收是都需要还是只需要其中一个。然后对应的根据引脚定义表来初始化对应的GPIO口。

, F, y  A) ?; R: E, ?# h  y
USART2对应的引脚
" ^' G$ E% \/ g1 r4 s2 ?; x' c4 {
微信图片_20230726144153.png   F5 m, D! B# [' u  T2 L

. E0 k' p/ r8 G+ m+ m1 k
USART1对应的引脚

( O8 w' X3 k, K- h
微信图片_20230726144154_4.png
) d) {+ z! P- L5 B% q$ \$ m
8 t0 i5 R  U2 L9 k  K
这里根据手册来看,RX引脚模式配置成浮空输入或者上拉输入。TX引脚模式配置成复用推挽输出。
; ]: t9 q* ^3 m. {% M/ r
**比如我这里只初始化TX发送端**
  1. //TX端/ w, S- t. m7 ]' Y' Z
  2.     GPIO_InitTypeDef GPIO_InitStructure;$ v; i8 c, j5 T2 Y, R* V
  3.     GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_PP;  //复用推挽输出
    # z( H' n( Y3 s7 l1 \5 ^% F% a5 ~
  4.     GPIO_InitStructure.GPIO_Pin=GPIO_Pin_2;   //USART2对应的TX端为GPIOA2
    0 l) D/ r% L, Z, q: z- t& n7 E9 Z- F
  5.     GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;  //50MhZ( i# l2 H. I: |$ R2 D
  6.     GPIO_Init(GPIOA,&GPIO_InitStructure);
复制代码

) v- q7 @) z% j7 h3 N
3. 串口初始化
1 r1 E" \4 v9 f7 q4 K7 u: |
注意哈,USART_Init()这个函数,是用来配置串口的相关参数的。

* `0 P: @; r: n- b( R
  • USART_BaudRate 串口通信使用的波特率 一般是9600或者是115200,这里我们给9600
    ' v% j( O  v2 d# q* {
  • USART_HardwareFlowControl 是否选择硬件流触发,一般这个我们也不选,所以选择无硬件流触发。
    5 ^) X! _( H2 L/ W- p
  • USART_Mode 这个参数要注意了哈,串口的模式,发送模式还是接收模式,还是两者都有
    9 u: E& _; U3 f1 g3 Q1 I0 n1 L
  • USART_Parity 校验位,可以选择奇偶校验和不校验。没有需求就直接无校验

    , d" ~6 e$ n* J" K( {
  • USART_StopBits 停止位 有1、0.5、2位,我们这里选1位停止位
    8 ~! O4 d5 Q, p5 \) U3 m
  • USART_WordLength 数据位 有8位和9位可以选择
    , J4 q* P6 n. C6 x
  1. //串口初始化
    7 I9 D- ^. x/ X) R2 O
  2.   USART_InitTypeDef USART_InitStruct;  ]8 i- |. u1 ]5 `/ ]4 R! l  V
  3.   USART_StructInit(&USART_InitStruct);  //初始默认值. Q, m1 X8 b* o, W/ ^! N
  4.   USART_InitStruct.USART_BaudRate=9600;
    5 _' S% N) J! r/ D. o3 x9 n- Q
  5.   USART_InitStruct.USART_HardwareFlowControl=USART_HardwareFlowControl_None;   //不使用硬件流触发& c; I; l/ r( o+ ?" J# G4 d" M) J
  6.   USART_InitStruct.USART_Mode=USART_Mode_Tx;         //TX 发送模式* K( w" Q3 u5 u: L+ w* k+ m
  7.   USART_InitStruct.USART_Parity=USART_Parity_No;       //不选择校验8 g) O/ z! s* l* v& V
  8.   USART_InitStruct.USART_StopBits=USART_StopBits_1;      //停止位1位' j3 G9 N6 Y; e1 \; e
  9.   USART_InitStruct.USART_WordLength=USART_WordLength_8b;  //数据位8位4 Y& M- K; |# K0 L% H
  10.   USART_Init(USART2,&USART_InitStruct);
复制代码

, E/ D' [- h4 ]& b( q
4. 串口使能
  1. //串口使能- P) d9 e* q! a
  2.    USART_Cmd(USART2,ENABLE);
复制代码
# l/ ?+ U9 @5 G9 t
5. 串口发送数据
/ W( O0 u6 G( g. n0 A0 U3 x
注意哈,我们要判断TXE标志位的状态。0,数据还没有被转移到移位寄存器;1,数据已经被转移到移位寄存器。

, u; F" a* W$ I4 ~: _+ p; M
当TXE标志位为1时,就说明可以发送下一个数据了。详细过程可看上面串口发送的解释。
  1. void Serial_SendByte(uint16_t Byte)' q7 M% |: y1 Y% u* A  G2 O
  2. {. X: n+ b- _% m
  3.   USART_SendData(USART2,Byte);
    3 a, X' f8 L4 T0 C) G1 c$ v
  4.   //0 表示数据还未转移到移位寄存器 循环等待 1 数据已经被转移到了移位寄存器可以发送数据2 |! H0 N$ n6 o( C; P' g
  5.   while(USART_GetFlagStatus(USART2,USART_FLAG_TXE)==RESET);  //不需要手动清零 再次写入TDR时会自动清零  
    0 R- I. @. r8 Y, G9 U5 P( E9 |2 Q
  6. }
复制代码
6 Z- M/ K8 H- [1 P1 a+ C
经过上述五步的配置,单片机就可以通过串口发送数据了。
& S4 P9 I* h, g
下方为发送数据图例,STM32向串口发送0x16数据

0 |% H! r: j+ Y0 N8 o( t) p1 R
微信图片_20230726144154_3.png
% Q4 }$ r9 p+ O8 x8 m% l+ v: J/ c* D' n9 p
5 q# \! u& H. m" ^
四、串口接收的两种实现方式
上方是发送数据的例子,那么串口接收又该如何配置,又要在串口发送的例子上做哪些更改呢?
* Y1 I: N8 d7 w0 v2 h/ ?5 s# Y: e
这里我们可以通过查询或者中断的方式来进行接收数据的两种方式。

1 [  l+ d5 v0 m3 c# p
  • 查询方式就是通过不断的查询RXNE标志位,通过判断RXNE位的状态来确定数据是否接收。

    % {; o) U, r  l( F, d
  • 中断方式就是通过配置接收输出控制通道,配置NVIC,在中断服务子函数里进行数据的接收。
    - I, G8 U3 d: b' d% t' u

4 ?" R6 {0 u5 j8 h$ ?
1. 需要更改的地方* w* j/ f, {2 b

  w  _. m- }; s: K1 g% ?1 F
既然我们要实现串口的接收,那么就要配置串口RX引脚,在串口模式中添加USART_Mode_RX模式。

+ t& V$ m4 j, {& F, l8 a' I
  • 初始化RX引脚

    # e7 U1 b. O+ z7 \6 h
  1. //RX端
    * f. S4 O, ^4 p
  2.   GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IPU;  //上拉输入  M& ^9 g& O* Q3 f( i# M
  3.     GPIO_InitStructure.GPIO_Pin=GPIO_Pin_3;( U: W# s- a$ r7 Y! l# }) p* ]
  4.     GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;  //50MhZ( K! l5 q: n: W/ t  j3 L2 |& y
  5.     GPIO_Init(GPIOA,&GPIO_InitStructure);
复制代码

  }2 e3 M( I, f6 \( W+ r- |: M
& L' o6 F$ {9 D0 l: r# S# Y/ x9 W
  • 添加串口模式

    8 w- R: {# u; N- K7 M4 o6 W/ }
  1. USART_InitStruct.USART_Mode=USART_Mode_Tx|USART_Mode_Rx;         //TX 发送模式  RX 接收模式
复制代码
+ o1 k: y1 d5 Y/ v; o
: q0 |+ p8 v' f: x
2. 查询RXNE标志位
; i9 Z. v+ q3 `6 f% f. U5 f! W" J
这里我们还是来看一看RXNE标志位的描述

% `! ~  b# X% ^  Z) e, B9 {. D# {
微信图片_20230726144154_2.png . {# _7 ]  L8 o: a0 h/ P2 x
4 y5 W: R! {! f  A
上图描述,为0时数据没有收到,为1时收到了数据,数据可以从RDR里读出

. N6 O) I& e! K) U9 j. g/ T( e
所以在主程序里不断读取RXNE标志位,如果为1,表示数据可以读出
  1. uint8_t RX_Data;7 B; Z" N) V5 g: i6 I) ]2 e" |
  2. int main()
    ! y' D) {) B" a2 j, x$ Z1 G
  3. { ; \1 P# M5 \9 I! `
  4.     Serial_Init();
    - e5 v1 `" h9 K. D6 e* o0 K3 g
  5.     Serial_SendByte(0x16);: f. a4 M. J. F% o# D" Q
  6.     while(1)
    9 I' Q% @% G1 Y
  7.     {
    ( t& Q% S3 R! w8 N; b7 f
  8.         if(USART_GetFlagStatus(USART2,USART_FLAG_RXNE)==SET)   //0 循环等待 1 可以接收数据
    . w6 H" G0 C4 g! W! Y
  9.         {
    , S( a0 V2 T3 ?* \" N- M
  10.             RX_Data=USART_ReceiveData(USART2);           
    + W8 @! ?. v& _" p5 Z0 a
  11.       Serial_SendByte(RX_Data);* y( c' c, A6 N& C# a! c
  12.         }* `* ]2 e3 {7 x
  13.     }# A! R$ s) p/ @' X& y0 }3 x
  14. }
复制代码
+ j- k" p  N; S
下图为程序现象:pc向单片机发送数据0x15,单片机接收数据0x15,并且把接收到的数据作为数据发送到pc,在pc上显示0x15。

- N2 v& `4 j6 z- {
微信图片_20230726144154_1.png
3 a0 U/ D- v$ O9 F

2 M8 Z# l( E' ^9 v& ^" f( |; i4 e9 g7 d
3. 使用中断
( A4 S# o7 {0 e+ b8 Q
  • 通过配置串口的接收作为中断源,开启中断输出控制,配置NVIC。开启中断通道。

    : J0 N- ?5 o; F, H3 u3 D5 U* a
  1. //开启中断输出控制$ q, ]4 R0 ]+ d' p3 D9 h9 J
  2.   USART_ITConfig(USART2,USART_IT_RXNE,ENABLE);; u4 ?1 ]+ B( \: u7 V3 ?3 o
  3.   6 ^' m- @2 d" ~/ `; g* |3 q2 D
  4.   //配置NVIC
    . X9 x5 ]+ u  X* Z2 z7 |  Y
  5.   NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);   q8 i, ]" _: E% O- Y1 s
  6.   NVIC_InitTypeDef NVIC_InitStruct;
    0 }2 s; v$ O' o2 |8 w3 W, @
  7.   NVIC_InitStruct.NVIC_IRQChannel=USART2_IRQn;   //选择USART2的中断通道
    0 U2 I0 y5 f+ D" [1 d
  8.   NVIC_InitStruct.NVIC_IRQChannelCmd=ENABLE;    //中断使能
    0 J5 b/ ~" `" J# \
  9.   NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority=1;
    0 G- ?3 `- v" j% ?! I9 \4 E/ C# j0 D
  10.   NVIC_InitStruct.NVIC_IRQChannelSubPriority=1;2 {% P4 o' P* W* C
  11.   NVIC_Init(&NVIC_InitStruct);
复制代码
% B7 _* }8 X3 \- n7 w

8 L+ x. p9 w* e, u- R% A
  • 中断服务子函数

    4 x) ]$ k1 D5 `$ Y: F& c: X
! O* N2 Q" v) L
中断服务子函数写好后,就可以在中断里读取接收到的数据了。

! t, y4 t# Y: X( L+ l6 [; Y当接收到数据后,触发接收中断,主程序暂停执行。接收完数据后主程序回复执行。当接收到数据时,就触发中断。
  1. void USART2_IRQHandler(void)* T" e. ^" p2 O  I
  2. {
    0 t! A, j' k) \+ j" T1 M) F( \
  3.   if(USART_GetITStatus(USART2,USART_IT_RXNE)==SET)   //RXNE 标志位为1 表示可以接收数据9 [$ O5 Q3 u# ^2 d! E" K  o
  4.   {; L, T7 @& e- q2 \' K1 z. h3 W
  5.     RX_Data=USART_ReceiveData(USART2);
    3 X" y9 |. X; s
  6.     Flag=1;* p- z# p5 P, ~# P' }% l
  7.     USART_ClearITPendingBit(USART2,USART_IT_RXNE);  //清除RXNE标志位# N: ^) S8 B8 w5 S- C. V, i
  8.   }* z+ F8 g0 Z2 v5 T6 ^% r% @& t
  9. }
复制代码

& w4 g6 s% U( d, H
  • 主程序测试
    ! X. L! A) k) e: E! \% w$ j
  1. uint8_t RX_Data;' M( X3 a3 y) G
  2. uint8_t Flag;
    " o, s1 X; S7 o" Y, x1 t

  3. 0 m9 S4 q0 k6 D+ |  N
  4. int main()) ~- w" w( I9 z" I
  5. {9 y( }: N7 i/ H+ C5 M) G5 Q- t
  6.     Serial_Init();7 V- P3 l" }# k8 v7 H
  7.     Serial_SendByte(0x16);  C" J* Q: ]6 L* o3 h& a

  8. ! E; M' E) y3 n7 Y7 O

  9. # {3 K% u' ~6 R, A
  10.     while(1)
    / q+ r) u# Z( T5 S3 m$ K
  11.     {7 h% P( ~: k7 P
  12.         if(Flag==1)
    3 C  r% y& Y# d1 h8 X" `! E& n% b
  13.         {
    # G+ E6 I, X8 c
  14.             Serial_SendByte(RX_Data);
    ' o0 |/ `' P3 C( z' H  k
  15.         }) m8 b, K3 [% k3 l5 o
  16.     }% K: Z# R) {7 h
  17. }
    " i  C/ P5 b# p  R# q

  18.   d' ~& ?" I* X! u9 ^
  19. void USART2_IRQHandler(void)
    . d( |; E; \8 X; h
  20. {
    * r9 P# r/ D3 V. Z2 L0 ?4 L
  21.     if(USART_GetITStatus(USART2,USART_IT_RXNE)==SET)   //RXNE 标志位为1 表示可以接收数据. ~/ m! M- A( N
  22.     {" A; h; O% n# q7 }% ]3 F
  23.         RX_Data=USART_ReceiveData(USART2);
    , m& r5 U9 A" j% k# k
  24.         Flag=1;: \/ g7 K% C( b
  25.         USART_ClearITPendingBit(USART2,USART_IT_RXNE);  //清除RXNE标志位
    : @" F5 |  q3 z- \) D2 b" o" u
  26.     }
    # U3 {; o3 E% f
  27. }
复制代码

7 F3 \; f8 H4 R
" W2 A' N$ M! ?( B$ T  j
下图为程序现象:可以看到,串口确实收到了数据,只是我把接收到的数据0xFE放在了while循环里,这说明数据接收是成功的,使用中断是可行的。
5 I) K. I# q+ S! w8 w
微信图片_20230726144154.png , S1 N! d. R, ]2 q
8 p8 y2 U5 y9 O& m

2 a% x  s7 Q" M, Z8 T
总结
到这里,就大致总结了串口的发送和接收。

( n6 Z/ }5 y3 _: z5 k+ R2 P
串口的配置,使用查询或者中断来接收数据。
4 o& V5 D( y5 g6 N6 _/ @
串口的使用会很常用到,所以在这里对串口做一个总结,也算是对之前知识的一个回顾和总结,加强印象。

" y! {( j8 T3 y5 O8 q
如果有什么写的不对的地方,欢迎指正!
: j+ h5 y/ j; G: E
转载自: 古月居
如有侵权请联系删除

/ _8 m9 W2 N) d- s- J! r  c; i
; ?  j- B2 D2 M7 X' u

6 ]9 E0 N! p0 ]5 a5 P( t2 `
收藏 评论0 发布时间:2023-7-26 14:43

举报

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