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 串口可以说是最古老,而且生命力最强的一种通信接口了。RS485总线更是久经考验。虽然串口早已经从大多数PC的标配中去掉了,但是嵌入式系统跟上位PC机通信用的最多的应该还是通过串口转USB吧。3 [: N% p" w: i3 r7 r ! a4 K3 Z! Q, F( Y N0 ~5 j 我们用 Keil 打开下面这个工程: 5 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发送的数据。 / N3 @, L2 J& Z/ ]6 ] 2.UART 的初始化 1 ~" h- b' V9 a8 a 我们看一下代码,串口参数的设置是在主程序里完成的,还有一部分是在stm32f0xx_hal_msp.c 里完成的。为什么要这么费事儿,而不把初始化代码全放在一个主程序里完成呢? $ B2 m( O( _ N2 Y$ p/ ` 我们要慢慢体会这样做带来的好处。我们调用一个驱动时,这个驱动难免会跟底层硬件打交道,比如串口驱动,它最终是利用用户选择的某一个串口模块,和与此模块连接的收发引脚进行数据收发的。 HAL(Hardware Abstract Layer) 把跟具体硬件细节相关的代码单独剥离了出来,并在Cube库中引入了 MSP(MCU Support Package) 的概念, 具体的硬件细节交给用户在这里面配置。 1 X4 K: Q0 K6 ^$ E HAL库里面对应每个硬件模块有两个函数 例如:: T8 z" u) A; | 6 [" A$ `! D7 X0 G6 G% Y* | HAL_UART_Init( ) 功能上的描述:设置收发模式、奇偶校验位、停止位数等等(与芯片无关)。 HAL_UART_MspInit( ) 硬件的描述: IO初始化,不同芯片,不同引脚设置不同。 % 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( ) 里配置一次就好了,之后不需要频繁的改变这些代码。 % G5 C `- J5 C' |2 | ; P4 h5 w' f( q; _3 {, L+ p* m 3.熟悉 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 B3 ?8 J( Q& }% \ typedef struct __MY_TypeDef 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; 5 Q$ e; @% {7 L5 [: k3 ? }MY_TypeDef; h4 f4 V3 t% t$ J MY_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 需要注意 MY_VAR->Var4 是个字节型指针变量,这个变量本身占用4个字节,它的值是 0x20000018, 而2 ?* x6 t3 V. b6 j1 [ 6 W& a1 z+ o2 U( s * MY_VAR->Var4 的值是 0x02。 把 MY_VAR 指向另一个地址: 3 Y1 T3 H% q) e1 C4 J8 M MY_VAR = (MY_TypeDef*) 0x2000001C; 与此类似,对于串口模块,驱动定义了一个结构体类型 UART_HandleTypeDef,我们可以用这个类型定义多个结构体,并通过把串口模块寄存器区的起始地址付给一个结构体,使该结构体和串口之间建立起联系: - v1 |/ d; g5 ~/ P. K5 t/ U1 o) Q/ W 6 M* G, D, E, p7 l0 k 我们运行的当前程序操作串口的方式为查询(polling)方式,结构体中和DMA,中断方式相关的内容可以先忽略,只需要关注结构体中下面这些成员即可:3 D1 S* u1 \# E1 F8 o USART_TypeDef *Instance;4 S# ?3 K1 ?8 a9 }' k( t USART_TypeDef 类型的指针,需要指向欲操作的串口寄存器区起始地址。以把此 Handle 和该串口建立起联系。! P# K( c" h" [8 ? UART_InitTypeDef Init; : n# p) R: }/ g( K: P% U 在调用初始化函数前,需要把初始化参数如 波特率,是否奇偶校验等写入此结构体。 * n' K1 @% M5 S( ]: h' w6 h UART_AdvFeatureInitTypeDef AdvancedInit; 串口扩展功能初始化参数。当前未用到扩展功能。% 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 ~ HAL_UART_Transmit(&UartHandle, (uint8_t*)aTxBuffer, TXBUFFERSIZE, 5000);2 D/ n7 c) n; }7 U &UartHandle 为 UART1 对应的 Handle 的首地址。 : Z: D3 k0 C1 U$ w Handle 除了保存自己对应模块的参数信息,还保存缓冲数据,以及当前工作状态。它可以保证各模块之间互不干扰,在代码执行过程中被打断,恢复后又可以正确继续执行。这样也便于把驱动集成到操作系统中。在以后的中断方式和 DMA 操作模式中,我们可以更深刻的体会到这种方法的优点。在理解了串口模块的工作方式后,理解其它模块就非常容易了。' A4 a; i# H" w8 X5 g 需要提到的是,在M0芯片内,有一些共享的或系统级的硬件模块不使用 Handle 的方式来处理: GPIO' [9 U2 |7 k5 r! e8 e" \ 0 r" f6 @* S, u9 s( }# |* f SYSTICK , K2 A. {0 L( H9 J NVIC PWR* Y% F) r5 r3 d0 D RCC" @. X* U' V+ i: I FLASH. |
【经验分享】STM32F1和STM32F4 区别
【经验分享】STM32F1系列之常用外设说明
【经验分享】STM32介绍
【经验分享】STM32F1x系列——Flash 模拟 EEPROM
【经验分享】STM32F1在MDK下新建标准库函数工程
【经验分享】stm32f1的存储器与复位
【经验分享】STM32F10X-架构
【经验分享】stm32F1 us延时函数
【经验分享】STM32F1之定时器
【经验分享】【stm32】stm32f1代码中core_cm3、system_stm32f10x、stm32f10x_conf、stm32f10x等文件的作用