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

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

[复制链接]
STMCU小助手 发布时间:2021-11-25 15:00
1.从 GPIO 到 UART; o' R: L* D; k, [! ~- [7 `; @4 Q6 Y
, W$ M* y! v- T( c
前面几节我们讲了MCU如何启动,如何用翻转IO引脚,以及用按键去触发中断。接下来我们介绍的也是最常用的一个模块,串口(UART)。( H& m5 x4 @$ b& m

( u$ W8 I, V3 R- t/ g串口可以说是最古老,而且生命力最强的一种通信接口了。RS485总线更是久经考验。虽然串口早已经从大多数PC的标配中去掉了,但是嵌入式系统跟上位PC机通信用的最多的应该还是通过串口转USB吧。3 [: N% p" w: i3 r7 r
! a4 K3 Z! Q, F( Y  N0 ~5 j
我们用 Keil 打开下面这个工程:
: i  k' A& e9 a) N) w5 j# f, U/ V- B# a7 F+ ^% v1 T# g
STM32Cube_FW_F0_V1.11.0\Projects\STM32F030R8-Nucleo\Examples\UART\UART_TwoBoards_ComPolling\MDK-ARM\Project.uvprojx  v3 o0 @0 r! I0 F% z
1 l! x& h/ W" [. e% f/ ]
这个代码配置串口为 9600,8 N 1,我们把代码编译下载后,可以通过 UART to USB 转换器连接到 PC 的 USB 口,在PC端用串口观察MCU发送的数据。
1 N2 u# u% Y- O- g/ N3 @, L2 J& Z/ ]6 ]
20200605173429966.png
6 A( W* r9 g; Z8 j+ r& X
20200605173429770.png
0 W$ y8 }5 i( n' l3 Z+ m

) E" V/ J9 `) K8 o1 X+ o" u
! Z# c- i. Z& d2 d, {2 S2.UART 的初始化
" h2 Q  B0 z4 c5 x6 Z* g% e1 ~" h- b' V9 a8 a
我们看一下代码,串口参数的设置是在主程序里完成的,还有一部分是在stm32f0xx_hal_msp.c 里完成的。为什么要这么费事儿,而不把初始化代码全放在一个主程序里完成呢?
1 V& U8 J8 I% B( E# |$ B2 m( O( _  N2 Y$ p/ `
我们要慢慢体会这样做带来的好处。我们调用一个驱动时,这个驱动难免会跟底层硬件打交道,比如串口驱动,它最终是利用用户选择的某一个串口模块,和与此模块连接的收发引脚进行数据收发的。
; K2 ~6 z( [; [) {/ l
7 Z- z" J0 Q6 f- GHAL(Hardware Abstract Layer) 把跟具体硬件细节相关的代码单独剥离了出来,并在Cube库中引入了 MSP(MCU Support Package) 的概念, 具体的硬件细节交给用户在这里面配置。
  L8 h! e% F" _; F$ L9 R2 B: {/ R1 X4 K: Q0 K6 ^$ E
HAL库里面对应每个硬件模块有两个函数 例如:: T8 z" u) A; |
6 [" A$ `! D7 X0 G6 G% Y* |
HAL_UART_Init( )  功能上的描述:设置收发模式、奇偶校验位、停止位数等等(与芯片无关)。
$ `! w* J) T9 O9 s  n: |- N
, M% F: P, H2 m  y# tHAL_UART_MspInit( )  硬件的描述: IO初始化,不同芯片,不同引脚设置不同。
' N# S& u" c3 U" r
6 k5 B( I5 A4 h( X$ ~$ t
20200605173429617.png
* g1 S4 o( J. [8 m
% Q* d: e0 c7 F. F7 T
回到程序,我们要使用串口时要调用驱动层的初始化函数 HAL_UART_Init( ),这个初始化函数回过头来调用了 HAL_UART_MspInit( ) 这个函数来完成 UART 时钟和收发引脚时钟的使能,以及收发引脚的配置。之后初始化函数继续进行 UART 端口的参数配置。- v  N0 n6 s- v6 A, m8 {0 ~
- A" E( |& H( Y" j2 p, A
这样做的一个好处就是使驱动层的初始化函数与硬件无关。一般我们做好一块板子后,所用的串口和引脚也就固定下来了,在 HAL_UART_MspInit( ) 里配置一次就好了,之后不需要频繁的改变这些代码。
0 }% x7 u8 ]8 c  I
+ B# w$ {( }9 ?" ~
20200605173429721.png

0 O1 g( |4 d- }. }, e
7 j' @! S% a& s! u  y" k% G5 C  `- J5 C' |2 |
20200605173429793.png
6 I$ v! R* h; }% z; U. h7 v2 B6 f* o
; P4 h5 w' f( q; _3 {, L+ p* m

1 j! p, i$ K8 j% {! h! N& b3 R3.熟悉 Handle
$ I) y7 r! F- {# y
$ B$ N" U( b* l2 t2 j5 ]
跟 GPIO 的初始化有所不同,在UART这个模块引入了 Handle 这一概念。在看 Handle 之前我们先熟悉一下在驱动里经常用到的结构体及其指针的用法:; |% x5 A0 w0 ~
. v9 f1 _6 }: X8 U: z! l2 H
20200605173429636.png
* a3 T* U# V3 ?( s
  B3 ?8 J( Q& }% \
typedef struct __MY_TypeDef
' e! G$ H1 i/ W4 Q# c9 x% c  v' w7 ^; h; x1 Z2 F: c3 e, e; x
{8 J* d% H( K# ]+ a
    uint8_t    Var1;. @+ C4 a% c( x) @
" N1 B# e/ z# r* W0 K$ c( H
    uint8_t    Var2;7 {& W! \5 ~. y7 v( |' L9 z3 G! o
- O$ @' w* r  J* K$ n  r6 Q# n
    uint16_t   Var3;4 a$ H5 ]$ k4 ]! E7 a8 n; N: r
/ l) o5 a( V! L6 G$ A$ X
    uint8_t*   Var4;
9 d7 X0 K) _+ ~5 H/ V8 u5 Q$ e; @% {7 L5 [: k3 ?
}MY_TypeDef;  h4 f4 V3 t% t$ J

/ `+ J1 a. A6 v. `. d1 gMY_TypeDef* MY_VAR;) x2 B0 Z! _+ y' i
8 v" @( D5 d2 w2 A8 T6 C& z
MY_VAR 是一个 MY_TypeDef 类型的指针,我们看看把它指向不同的地址时会发生什么?- F7 z' H; s8 r, r  H; ^0 _( r
4 o1 n. B' N" g# w" |  p
MY_VAR = (MY_TypeDef*) 0x20000018;4 s7 {( n" f( z! o
% i) p6 ?& N, N; `  S8 V
20200605173429681.png
) c( c# U. a' L7 h$ C* j

7 s7 H7 }1 S0 X1 a- u* d0 U需要注意 MY_VAR->Var4 是个字节型指针变量,这个变量本身占用4个字节,它的值是 0x20000018, 而2 ?* x6 t3 V. b6 j1 [
6 W& a1 z+ o2 U( s
* MY_VAR->Var4 的值是 0x02。
9 t& W7 I: w2 N
4 t# h% K- y. z/ s4 g把 MY_VAR 指向另一个地址:
. c: D8 c6 D0 s5 h. R3 Y1 T3 H% q) e1 C4 J8 M
MY_VAR = (MY_TypeDef*) 0x2000001C;
$ b$ |( i/ W& O4 n4 j2 s. P4 b
! H6 o9 v& J" Q! {. N% N
20200605173429700.png
' ]: }& E. B( ]; `* Y9 L- M( ?

  e6 O- J7 M" u' |7 [$ H3 ~- {与此类似,对于串口模块,驱动定义了一个结构体类型 UART_HandleTypeDef,我们可以用这个类型定义多个结构体,并通过把串口模块寄存器区的起始地址付给一个结构体,使该结构体和串口之间建立起联系:
6 G0 U( i0 V9 {5 ]6 J- v1 |/ d; g5 ~/ P. K5 t/ U1 o) Q/ W
20200605173429768.png
  }5 B' S. m7 y2 V
6 M* G, D, E, p7 l0 k
我们运行的当前程序操作串口的方式为查询(polling)方式,结构体中和DMA,中断方式相关的内容可以先忽略,只需要关注结构体中下面这些成员即可:3 D1 S* u1 \# E1 F8 o

( U' w0 Z/ q9 P5 a/ W8 XUSART_TypeDef  *Instance;4 S# ?3 K1 ?8 a9 }' k( t

5 [' S6 O) @: A' f  l6 X- R  U6 O8 j, jUSART_TypeDef 类型的指针,需要指向欲操作的串口寄存器区起始地址。以把此 Handle 和该串口建立起联系。! P# K( c" h" [8 ?

3 L; O1 M  s. `( W, j8 UUART_InitTypeDef   Init;
9 [1 d/ ]1 N- V% n  P5 x; I( V: n# p) R: }/ g( K: P% U
在调用初始化函数前,需要把初始化参数如 波特率,是否奇偶校验等写入此结构体。
: u' }$ u: H1 @/ F/ D& B* n' K1 @% M5 S( ]: h' w6 h
UART_AdvFeatureInitTypeDef  AdvancedInit;
2 a1 i6 o) z2 f$ Y+ X' y
, q3 k: a% z8 e: Q  @$ n  a, f串口扩展功能初始化参数。当前未用到扩展功能。% T/ x2 V7 J' u$ X. a( K3 q- v% D
9 O3 l  @" O3 c& t* R3 Q
使用 Handle 的好处是,我们操作某个模块时,把这个模块对应的 Handle 的首地址传给驱动函数就行了。此函数通过 Handle 就可以找到所有需要的东西。如:( t1 X# ?+ W. r8 e/ E+ @( W2 ~

7 P: ^+ g1 `" e% L2 V5 ?HAL_UART_Transmit(&UartHandle, (uint8_t*)aTxBuffer, TXBUFFERSIZE, 5000);2 D/ n7 c) n; }7 U

* f& S) _/ x1 }$ L1 ?$ W. x% N&UartHandle 为 UART1 对应的 Handle 的首地址。
6 h0 d( c( C, g8 U2 X! G9 j& c: Z: D3 k0 C1 U$ w
Handle 除了保存自己对应模块的参数信息,还保存缓冲数据,以及当前工作状态。它可以保证各模块之间互不干扰,在代码执行过程中被打断,恢复后又可以正确继续执行。这样也便于把驱动集成到操作系统中。在以后的中断方式和 DMA 操作模式中,我们可以更深刻的体会到这种方法的优点。在理解了串口模块的工作方式后,理解其它模块就非常容易了。' A4 a; i# H" w8 X5 g

& O2 [5 x" N; s/ f需要提到的是,在M0芯片内,有一些共享的或系统级的硬件模块不使用 Handle 的方式来处理:
, }. L; f# j% C4 \# z
6 o5 U- z! G. H7 \) `$ fGPIO' [9 U2 |7 k5 r! e8 e" \
0 r" f6 @* S, u9 s( }# |* f
SYSTICK
- |, V  ~# E8 b. e% Z# r% h, K2 A. {0 L( H9 J
NVIC
9 ~  i& D' X/ p4 E4 F+ k0 e) c4 [* z
$ l& d( l& B- j$ ~5 oPWR* Y% F) r5 r3 d0 D

& V  s- M9 O4 g; ORCC" @. X* U' V+ i: I

: ~/ Q8 G/ J/ J& o1 L# D: T5 q) wFLASH.
) S' c* X, ?4 z+ I6 M- m& j' x
' i9 y5 [2 G) B7 c8 E) |- w7 K* s( u
  q4 T% M6 A5 e* P
收藏 评论0 发布时间:2021-11-25 15:00

举报

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