1.从 GPIO 到 UART 前面几节我们讲了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 我们用 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 2.UART 的初始化0 P+ G1 d/ q3 z0 Y ( F3 ^( B2 \# K. b 我们看一下代码,串口参数的设置是在主程序里完成的,还有一部分是在stm32f0xx_hal_msp.c 里完成的。为什么要这么费事儿,而不把初始化代码全放在一个主程序里完成呢?$ C" T; v2 K( T 我们要慢慢体会这样做带来的好处。我们调用一个驱动时,这个驱动难免会跟底层硬件打交道,比如串口驱动,它最终是利用用户选择的某一个串口模块,和与此模块连接的收发引脚进行数据收发的。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) 的概念, 具体的硬件细节交给用户在这里面配置。 HAL库里面对应每个硬件模块有两个函数 例如:+ t! H) T+ j9 C' [3 ? 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 8 W: n! V/ T2 w. C# `% i' o5 N2 y) i 回到程序,我们要使用串口时要调用驱动层的初始化函数 HAL_UART_Init( ),这个初始化函数回过头来调用了 HAL_UART_MspInit( ) 这个函数来完成 UART 时钟和收发引脚时钟的使能,以及收发引脚的配置。之后初始化函数继续进行 UART 端口的参数配置。 这样做的一个好处就是使驱动层的初始化函数与硬件无关。一般我们做好一块板子后,所用的串口和引脚也就固定下来了,在 HAL_UART_MspInit( ) 里配置一次就好了,之后不需要频繁的改变这些代码。( B0 d; \5 S2 b: L! a; G . F3 f# d& n3 T$ W1 F* Q. n 3.熟悉 Handle+ v" }/ v7 w4 Q# i & j8 n* `8 C0 X$ Y 跟 GPIO 的初始化有所不同,在UART这个模块引入了 Handle 这一概念。在看 Handle 之前我们先熟悉一下在驱动里经常用到的结构体及其指针的用法:3 l* h! S# k% A$ y( ^ $ F3 K# K) S, V0 N$ z; h typedef struct __MY_TypeDef. R% M0 X# J# C1 R4 W6 { { uint8_t Var1; 4 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 uint8_t* Var4; }MY_TypeDef; MY_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 MY_VAR = (MY_TypeDef*) 0x20000018;* E+ \) d; X3 g ! j+ Z: D/ N$ k+ a9 | 需要注意 MY_VAR->Var4 是个字节型指针变量,这个变量本身占用4个字节,它的值是 0x20000018, 而 2 q8 b6 w* H3 m# m2 J/ P * MY_VAR->Var4 的值是 0x02。 把 MY_VAR 指向另一个地址: 1 x5 s! T% x2 x9 D; f% ]" ` MY_VAR = (MY_TypeDef*) 0x2000001C;$ h0 ^. O7 ?% M 与此类似,对于串口模块,驱动定义了一个结构体类型 UART_HandleTypeDef,我们可以用这个类型定义多个结构体,并通过把串口模块寄存器区的起始地址付给一个结构体,使该结构体和串口之间建立起联系: 我们运行的当前程序操作串口的方式为查询(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 和该串口建立起联系。 UART_InitTypeDef Init; 在调用初始化函数前,需要把初始化参数如 波特率,是否奇偶校验等写入此结构体。2 E3 z) j; M& j, u7 c UART_AdvFeatureInitTypeDef AdvancedInit; 串口扩展功能初始化参数。当前未用到扩展功能。 使用 Handle 的好处是,我们操作某个模块时,把这个模块对应的 Handle 的首地址传给驱动函数就行了。此函数通过 Handle 就可以找到所有需要的东西。如: HAL_UART_Transmit(&UartHandle, (uint8_t*)aTxBuffer, TXBUFFERSIZE, 5000); &UartHandle 为 UART1 对应的 Handle 的首地址。 Handle 除了保存自己对应模块的参数信息,还保存缓冲数据,以及当前工作状态。它可以保证各模块之间互不干扰,在代码执行过程中被打断,恢复后又可以正确继续执行。这样也便于把驱动集成到操作系统中。在以后的中断方式和 DMA 操作模式中,我们可以更深刻的体会到这种方法的优点。在理解了串口模块的工作方式后,理解其它模块就非常容易了。; u y+ y# f6 }+ h 9 b: q# `# u, k- T3 c! \8 ] 需要提到的是,在M0芯片内,有一些共享的或系统级的硬件模块不使用 Handle 的方式来处理: 5 C9 P; T3 a9 G! T' m9 s0 t _ GPIO# T& ~/ R9 g$ b1 C; n- T( H( a0 k SYSTICK. E/ Y5 m4 z9 y9 B ) a( E, w! J! k8 D4 l# ], N NVIC1 Z3 W. u( W3 }+ v. x3 V PWR ( q/ L9 t9 B! K" q3 A# ~1 C0 a- s RCC; p' D% O6 Q% A1 {$ n+ O/ E FLASH./ o; u) C0 a# `( r( _% s! k$ y . P9 C$ G5 r( g2 }8 o Q5 v |
【经验分享】STM32F1和STM32F4 区别
【经验分享】STM32F1系列之常用外设说明
【经验分享】STM32介绍
【经验分享】STM32F1x系列——Flash 模拟 EEPROM
【经验分享】STM32F1在MDK下新建标准库函数工程
【经验分享】stm32f1的存储器与复位
【经验分享】STM32F10X-架构
【经验分享】stm32F1 us延时函数
【经验分享】STM32F1之定时器
【经验分享】【stm32】stm32f1代码中core_cm3、system_stm32f10x、stm32f10x_conf、stm32f10x等文件的作用