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

【经验分享】STM32F0单片机快速入门六 用库操作串口(UART)原来如此简单

[复制链接]
STMCU小助手 发布时间:2021-11-25 15:00
1.从 GPIO 到 UART
- z5 X0 D$ l! E) s% R
% x+ N* u9 ]( X* p: _5 z前面几节我们讲了MCU如何启动,如何用翻转IO引脚,以及用按键去触发中断。接下来我们介绍的也是最常用的一个模块,串口(UART)。/ K3 C6 ^/ `  _. B3 U; R' q6 J
, k% U/ T0 B- }. K3 x" c
串口可以说是最古老,而且生命力最强的一种通信接口了。RS485总线更是久经考验。虽然串口早已经从大多数PC的标配中去掉了,但是嵌入式系统跟上位PC机通信用的最多的应该还是通过串口转USB吧。8 d; W7 j& a* @$ b6 l. O. H

6 X# Z: T, M, w. ]我们用 Keil 打开下面这个工程:% A7 W3 F, ^& O8 w% b
8 |0 j8 l0 C( O1 Y. y$ r2 A
STM32Cube_FW_F0_V1.11.0\Projects\STM32F030R8-Nucleo\Examples\UART\UART_TwoBoards_ComPolling\MDK-ARM\Project.uvprojx0 x9 c: M+ C3 z+ T
9 N2 G6 _, G' a8 i6 _
这个代码配置串口为 9600,8 N 1,我们把代码编译下载后,可以通过 UART to USB 转换器连接到 PC 的 USB 口,在PC端用串口观察MCU发送的数据。' w6 L4 e2 ]3 g3 F

$ k) g& L, }1 O; |# P' F& R2 e- h
20200605173429966.png
2 i& w, L6 Q; d6 Z- m, h
20200605173429770.png
9 E  b+ K, N; x  I4 A( r/ s

7 e* b* F! n7 ^, ]% f
" h- ~% {1 A0 y: S2 m2.UART 的初始化0 P+ G1 d/ q3 z0 Y
( F3 ^( B2 \# K. b
我们看一下代码,串口参数的设置是在主程序里完成的,还有一部分是在stm32f0xx_hal_msp.c 里完成的。为什么要这么费事儿,而不把初始化代码全放在一个主程序里完成呢?$ C" T; v2 K( T

, x) Z8 i) Q% |3 d0 a我们要慢慢体会这样做带来的好处。我们调用一个驱动时,这个驱动难免会跟底层硬件打交道,比如串口驱动,它最终是利用用户选择的某一个串口模块,和与此模块连接的收发引脚进行数据收发的。7 q; k1 e  W4 i3 F, h1 f1 {
  p5 H/ s, o/ u0 J3 K! |- g& Q
HAL(Hardware Abstract Layer) 把跟具体硬件细节相关的代码单独剥离了出来,并在Cube库中引入了 MSP(MCU Support Package) 的概念, 具体的硬件细节交给用户在这里面配置。
3 G9 H* T( V4 g. b* a5 }
3 y2 q  q; I5 h# Q3 \* KHAL库里面对应每个硬件模块有两个函数 例如:+ t! H) T+ j9 C' [3 ?

) H$ }8 B2 e" u7 b9 ^HAL_UART_Init( )  功能上的描述:设置收发模式、奇偶校验位、停止位数等等(与芯片无关)。$ V- n2 y8 P4 N
  J1 x  L6 X! f  J. d* t' G
HAL_UART_MspInit( )  硬件的描述: IO初始化,不同芯片,不同引脚设置不同。  O& j$ h0 w- ~6 b9 S7 V( u9 C
* K* O9 E, ~# }  K/ U
20200605173429617.png

4 \8 Y6 t$ N0 I8 f2 B) I1 A: P8 W: n! V/ T2 w. C# `% i' o5 N2 y) i
回到程序,我们要使用串口时要调用驱动层的初始化函数 HAL_UART_Init( ),这个初始化函数回过头来调用了 HAL_UART_MspInit( ) 这个函数来完成 UART 时钟和收发引脚时钟的使能,以及收发引脚的配置。之后初始化函数继续进行 UART 端口的参数配置。
& |7 y6 E9 Y& x8 U  a
; Q6 F1 G, K7 C9 o1 R这样做的一个好处就是使驱动层的初始化函数与硬件无关。一般我们做好一块板子后,所用的串口和引脚也就固定下来了,在 HAL_UART_MspInit( ) 里配置一次就好了,之后不需要频繁的改变这些代码。( B0 d; \5 S2 b: L! a; G

4 @/ `% ?; r' E/ S
20200605173429721.png
  S+ K# @0 |- r# s8 U
. F3 f# d& n3 T$ W1 F* Q. n

+ a2 h/ x1 V( `! d
20200605173429793.png

3 v# k- U) m4 {" }7 U1 B$ Y
6 }% s0 |3 ?6 i0 E9 {7 @
  W. C% i0 B5 v5 \3.熟悉 Handle
+ v" }/ v7 w4 Q# i
& j8 n* `8 C0 X$ Y
跟 GPIO 的初始化有所不同,在UART这个模块引入了 Handle 这一概念。在看 Handle 之前我们先熟悉一下在驱动里经常用到的结构体及其指针的用法:3 l* h! S# k% A$ y( ^

9 q8 ]" v, t/ W% j5 P  ]
20200605173429636.png
3 _: f: W* R+ B9 {! l
$ F3 K# K) S, V0 N$ z; h
typedef struct __MY_TypeDef. R% M0 X# J# C1 R4 W6 {

% s% L% @: K& V  {' i{
9 P9 a$ A  }5 E; H. n5 W    uint8_t    Var1;
" M) ~6 T7 `$ t* Y  R4 C4 O4 x+ ^8 p1 }2 d% b$ |
    uint8_t    Var2;) e) Q* K: H6 O4 @' t  B( y
1 L0 L: P6 _7 \9 B/ J  ?% D: l
    uint16_t   Var3;9 B& ^3 v5 @" N

: V0 V/ ^% n' Z5 j, Y( S! x    uint8_t*   Var4;
8 n( S$ i& @( B, y' v* J
. o- l5 @) H* y' s( f7 p% `* C}MY_TypeDef;
/ p) Q, o! Z8 f  @: F# o2 V
  R8 b# I5 j% ]- X% K, MMY_TypeDef* MY_VAR;  h1 ~+ {& Q% e% [
7 |4 J0 `& \# o* j* b7 |
MY_VAR 是一个 MY_TypeDef 类型的指针,我们看看把它指向不同的地址时会发生什么?7 O$ f2 U0 v1 V& o) k- y5 j

2 l" E4 j% t8 I0 A" ]! VMY_VAR = (MY_TypeDef*) 0x20000018;* E+ \) d; X3 g

6 B5 F% S3 r4 Y% V
20200605173429681.png

0 ~, U/ Q  v+ Q! j+ Z: D/ N$ k+ a9 |
需要注意 MY_VAR->Var4 是个字节型指针变量,这个变量本身占用4个字节,它的值是 0x20000018, 而
- }& `, |; @6 U% I3 _3 [; R9 g" y2 q8 b6 w* H3 m# m2 J/ P
* MY_VAR->Var4 的值是 0x02。
. \8 X3 U" y, E) W8 P6 A( R$ b) j
' X- j8 Y5 M" c6 }, p; i把 MY_VAR 指向另一个地址:
1 ?" }; ^6 b3 U% A2 W1 x5 s! T% x2 x9 D; f% ]" `
MY_VAR = (MY_TypeDef*) 0x2000001C;$ h0 ^. O7 ?% M

' l* J* D6 V7 p& L" w; J
20200605173429700.png
( f( `  L0 j- `6 L) U: ~

7 J3 Z( f9 m5 N! S/ C* d- s与此类似,对于串口模块,驱动定义了一个结构体类型 UART_HandleTypeDef,我们可以用这个类型定义多个结构体,并通过把串口模块寄存器区的起始地址付给一个结构体,使该结构体和串口之间建立起联系:
5 X# [( |3 b$ e7 ]- v5 o
2 e4 O( j' `, H" z! j) _
20200605173429768.png
, o0 ^7 w: m. p) `% W" f* X9 q' e

0 P" g6 ]2 A1 B' F我们运行的当前程序操作串口的方式为查询(polling)方式,结构体中和DMA,中断方式相关的内容可以先忽略,只需要关注结构体中下面这些成员即可:0 j: c5 a9 a0 s* E2 Q! n) @" z) C
; B6 r: t+ G/ Q( M! F
USART_TypeDef  *Instance;% ]1 ^+ _8 f" K+ B2 f2 I
! m( N0 k( H7 C! v
USART_TypeDef 类型的指针,需要指向欲操作的串口寄存器区起始地址。以把此 Handle 和该串口建立起联系。
' t5 O4 _% Q2 l- l  m9 V( R
1 c3 t' h& F8 o! o' c1 L1 xUART_InitTypeDef   Init;
% ^) n4 Z2 V  }% O5 }
& N/ q7 u3 z2 e$ e* c在调用初始化函数前,需要把初始化参数如 波特率,是否奇偶校验等写入此结构体。2 E3 z) j; M& j, u7 c

/ P( D  C5 _3 Y$ l' Z# F7 HUART_AdvFeatureInitTypeDef  AdvancedInit;
$ k+ |; O1 G) X: K9 \
# v. Z0 t5 a4 l8 p串口扩展功能初始化参数。当前未用到扩展功能。
1 |: m& k, i5 U
% b) X, Y& C* {3 \0 n使用 Handle 的好处是,我们操作某个模块时,把这个模块对应的 Handle 的首地址传给驱动函数就行了。此函数通过 Handle 就可以找到所有需要的东西。如:
% |. k8 n( Y; Y/ g
) y* d, V* i* q6 Z' c5 x$ OHAL_UART_Transmit(&UartHandle, (uint8_t*)aTxBuffer, TXBUFFERSIZE, 5000);
6 o5 i8 ?0 J9 t& a9 G" ?( R$ |
8 l1 ~5 U2 C# ^% ?, L7 F9 D) u&UartHandle 为 UART1 对应的 Handle 的首地址。
0 ~) m4 I# i3 W( C; ^. G- ^' \; m
1 k, {. u+ ^4 f4 y" ?Handle 除了保存自己对应模块的参数信息,还保存缓冲数据,以及当前工作状态。它可以保证各模块之间互不干扰,在代码执行过程中被打断,恢复后又可以正确继续执行。这样也便于把驱动集成到操作系统中。在以后的中断方式和 DMA 操作模式中,我们可以更深刻的体会到这种方法的优点。在理解了串口模块的工作方式后,理解其它模块就非常容易了。; u  y+ y# f6 }+ h
9 b: q# `# u, k- T3 c! \8 ]
需要提到的是,在M0芯片内,有一些共享的或系统级的硬件模块不使用 Handle 的方式来处理:
+ R* Y- ^6 o# n+ S  Y. l* D; e5 C9 P; T3 a9 G! T' m9 s0 t  _
GPIO# T& ~/ R9 g$ b1 C; n- T( H( a0 k

) ?7 A2 @$ k+ s4 W# ]( S2 NSYSTICK. E/ Y5 m4 z9 y9 B
) a( E, w! J! k8 D4 l# ], N
NVIC1 Z3 W. u( W3 }+ v. x3 V

# z. a# q1 f9 t: LPWR
6 Q% }- S4 W0 E/ t3 f' c( q/ L9 t9 B! K" q3 A# ~1 C0 a- s
RCC; p' D% O6 Q% A1 {$ n+ O/ E

' ?/ N& X0 K# e7 b9 b( i# K2 M& eFLASH./ o; u) C0 a# `( r( _% s! k$ y

' J6 m4 ~' u7 }' G$ p* L. P9 C$ G5 r( g2 }8 o  Q5 v
收藏 评论0 发布时间:2021-11-25 15:00

举报

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