一、GPIO介绍
! S& m$ H& [' t" p8 u/ D 1.1 GPIO 简述
2 b# s& u5 P: q( b5 \9 r GPIO(General purpose input/output,通用型输入输出),一个引脚可以用于输入、输出或其他特殊功能,PIN脚依现实需要可作为通用输入(GPI)或通用输出(GPO)或通用输入与输出(GPIO),并多个GPIO一起组合可实现诸如UART、SPI等外设功能。
' v& T+ F6 M& d7 a! R# V8 |. i* p+ G* e. ^
GPIO是通过寄存器操作来实现电平信号输入输出的,对于输入,通过读取输入数据寄存器(IDR,Input Data Register)来确定引脚电位的高低;对于输出,通过写入输出数据寄存器(ODR,Output Data Register)来让这个引脚输出高电位或者低电位;对于其他特殊功能,则有另外的寄存器来控制它们。GPIO的输出通常可以输出0/1信号,用来实现如LED灯、继电器、蜂鸣器等控制,而GPIO的输入可识别0/1信号,用来实现开关、按键等等动作或状态判定。$ k! [: ~ w& O* |% ^6 m
) {7 h k7 p8 u3 T: m/ T
; a: c$ d! `' u1 l2 E
2 S; L! R, `0 h8 y" N1 ]( W+ r6 a* E
1.2 STM32 GPIO寄存器7 @/ z+ X5 e. M
STM32中GPIO的寄存器是MCU分配的一个连续存储区域,按存储地址划分出不同功能寄存器,如下图所示,有Port mode寄存器、Output Type寄存器等等。
& V& @) t y {7 A
0 j& \1 `0 B0 a% m
2 M! d# D& G0 t; ], V! p7 c9 b
9 n+ ~9 G& x# q; W
2 \6 D5 H# X5 X 在HLA库中,这些寄存器的数值(宏定义)通常会放在/Drivers/CMSIS/Device/ST/STM32L4xx/Include/stm32XXX.h头文件中,供开发者调用,例如STM32L496VGTx的开发板,该头文件具体名是stm32l496xx.h,自8977行起到10008行,具是按上述表宏定义了GPIO各寄存器的各个数据位可设置数据值(偏移(POS)及相应数值)。+ K% _+ S( ?+ x$ ?! E3 L U
8 P* ]4 m. t) v) ?
( E' g ]& m4 B/ a, ^4 n- T
, y8 A, q: e8 @% m3 H
1.3 GPIO引脚与标记 {' g2 n4 i$ r* g2 _
在一个STM32 MCU芯片中,所有引脚本质都是GPIO引脚,只是鉴于全GPIO引脚开发者使用起来比较不方便,于是才通过各个GPIO组合形成针对各个外设的IO接口。在CubeMX中,以STM32L496VGTx芯片为例,可以直观看到该芯片有100个引脚:LQFP100,LQFP是封装,100指的是引脚数。
9 p/ z9 p% h" U5 }& V5 U) M/ F3 N0 t1 E& H, D; p
6 _7 m1 k6 d; |
8 K' @5 b2 n1 k: B7 D STM32的MCU标记一般有两种,直插IC以半圆条口为标记,贴片IC以小圆点为标记,也有缺口及小点都有标记的。通常将标记圆点或缺口正面朝左上角,左上角的下方脚为第一脚,依次围绕芯片中心逆时针计数,在旋转360度以后,左上角右方角为最后一脚。如上图所示,STM32L496VGTx引脚序号如蓝色字体及箭头所示标记。STM32的MCU的引脚标记有三种方式:
- j; R/ Y; i6 H
$ q) ?# d# V. O 一种是基于上面所述的100个序号排序P1、Pn等,这种标记目前一般是在直接操作寄存器地址或使用标准库中使用,目前HLA库、LL库已经不提供直接地址偏移方式的API了,而是给没一组分组提供了独立寄存器来处理该组引脚;% v$ a+ N& ~( P3 I: U2 k, | a
) A L# v7 R4 F" K4 i$ I6 b: e
第二种是标准库或HLA库宏定义了引脚分组,一般为A、B、C、...,按大写字母次序网线,目前支持到16组类别,每组为PX0~PX15,其标记就是分组名+0~15序号,例如GPIOB+GPIO_PIN_3。' A( l3 b' P/ \3 i' P. b
8 d! B# {, K% [( `4 W7 T3 L
注意,目前STM32的MCU设计是最大16组类别,支持到256引脚(16*16),通过STM32命名规范可以得知,STM32最做引脚支持到256个,但一般会少于引脚数/16的组数,因为不少引脚在芯片设计时就特别支持了其引脚标识,如VSS、VDD等。另外也不是每组必须0~15序号全用完,分组主要是方便设计及使用时进行引脚区分而已。6 d$ j. n" I/ H5 @' V7 W
! s+ M- h1 Y" v- v8 a2 X1 h; v' X2 E
3 k, D& v$ v5 S, B& {5 [. b* k8 h6 K4 j( c+ Z) O
目前HLA库主要就是这种方式,例如来看看GPIO读取数据的API(HAL_GPIO_ReadPin)实现,主要就是根据引脚序号(0~15)来读取IDR寄存器内对应数据位的数值。
! t: e) F/ [" v7 x/ y- l* W8 k: d8 r6 x5 e" X) M" j. f' G& N# Z
0 C& K9 m# [0 X
9 K! Y$ S, E5 o& g
GPIO分组引脚地址在HLA宏定义数值如下。
8 }. v& p. \# f6 y( ?( K& i x- #define GPIO_PIN_0 ((uint16_t)0x0001) /* Pin 0 selected */ P( n* a" R5 v4 M4 K6 D5 y# f
- #define GPIO_PIN_1 ((uint16_t)0x0002) /* Pin 1 selected */3 [ q& Y& p( U/ R
- #define GPIO_PIN_2 ((uint16_t)0x0004) /* Pin 2 selected */5 m# r T2 |- i1 s
- #define GPIO_PIN_3 ((uint16_t)0x0008) /* Pin 3 selected */
( m/ H& s h: R - #define GPIO_PIN_4 ((uint16_t)0x0010) /* Pin 4 selected */
+ t" f$ S' f- ` e6 G6 a# y5 [8 x - #define GPIO_PIN_5 ((uint16_t)0x0020) /* Pin 5 selected */! P2 w3 L" f( c8 ?6 W; u! G
- #define GPIO_PIN_6 ((uint16_t)0x0040) /* Pin 6 selected *// ]) A8 K( R' @* A3 a
- #define GPIO_PIN_7 ((uint16_t)0x0080) /* Pin 7 selected */9 A9 U, R, p9 m; J
- #define GPIO_PIN_8 ((uint16_t)0x0100) /* Pin 8 selected */$ |4 t7 n0 t0 C' S1 I/ J% i" m
- #define GPIO_PIN_9 ((uint16_t)0x0200) /* Pin 9 selected */
; O) w4 e) O3 R: G+ ]5 L ^9 E - #define GPIO_PIN_10 ((uint16_t)0x0400) /* Pin 10 selected */
' M+ x( o- Z7 l' f4 B9 W1 o - #define GPIO_PIN_11 ((uint16_t)0x0800) /* Pin 11 selected */
- c8 x% U" a9 Y5 p7 C# f; d - #define GPIO_PIN_12 ((uint16_t)0x1000) /* Pin 12 selected */
6 i5 l% ^' G: h$ r2 N - #define GPIO_PIN_13 ((uint16_t)0x2000) /* Pin 13 selected */2 `; N: C ?1 N8 _
- #define GPIO_PIN_14 ((uint16_t)0x4000) /* Pin 14 selected */
, k1 p: u; r/ @" ?7 C% n& o - #define GPIO_PIN_15 ((uint16_t)0x8000) /* Pin 15 selected */8 g- d* q$ b# O7 Z9 Y
- #define GPIO_PIN_All ((uint16_t)0xFFFF) /* All pins selected */
复制代码
6 C7 _+ U1 l/ U3 v. ` 而GPIO各个分组的IDR寄存器为32bit宽度的数据,目前只用到低16位字段。# k4 G: Z+ L+ @ w
- typedef struct" G5 w) |/ _: T% Q- _5 ^
- {
0 ?) H6 D$ i% R) m - __IO uint32_t MODER; /*!< GPIO port mode register, Address offset: 0x00 */% i k: _ {. e5 `
- __IO uint32_t OTYPER; /*!< GPIO port output type register, Address offset: 0x04 */
# Z" i* a i; D/ G - __IO uint32_t OSPEEDR; /*!< GPIO port output speed register, Address offset: 0x08 */ N) D ^+ d/ O
- __IO uint32_t PUPDR; /*!< GPIO port pull-up/pull-down register, Address offset: 0x0C */: c* @) K; G$ \8 p& s; k9 P
- __IO uint32_t IDR; /*!< GPIO port input data register, Address offset: 0x10 */
( v ], Y9 n" L6 @4 Q' [% k8 D - __IO uint32_t ODR; /*!< GPIO port output data register, Address offset: 0x14 */) ~( \" C" l# _, n
- __IO uint32_t BSRR; /*!< GPIO port bit set/reset register, Address offset: 0x18 */1 F$ M& M& G8 q
- __IO uint32_t LCKR; /*!< GPIO port configuration lock register, Address offset: 0x1C */: H- \5 `" r6 y q2 J
- __IO uint32_t AFR[2]; /*!< GPIO alternate function registers, Address offset: 0x20-0x24 */
3 i+ [8 _/ o( @: M% p - __IO uint32_t BRR; /*!< GPIO Bit Reset register, Address offset: 0x28 */
- @) p2 x- M @7 S/ @: E, j - 9 Q- c- k6 ]9 s9 p
- } GPIO_TypeDef;
复制代码
/ G$ G' T8 D4 P8 s 第三种引脚标记其实就是在第二种方式基础上,为引脚设置别名,在cubeMX设置引脚mode后,会开启该引脚,在引脚参数设置列表可以设置该引脚标识名,例如下图,设置PE3(P2)为输出模式(GPIO_Output),的别名为LED1。
* ~! G: m5 f6 C- M1 O& u
. z6 P S% K* e! P% Y6 h
2 W( `% D; P4 i. O9 P
3 n8 ]- L4 r/ p. y 那么在生成输出代码时,会通过#define实现标识名替换。& U- A" A/ E6 p% r
! J' s0 t5 m: y4 Q+ z. f
4 H2 i/ \( E" ?
; V. U+ X! y/ V3 \3 X& J7 Z% c
: w# A) a: c) Q7 \9 t& r 1.4 GPIO原理框图及CubeMX配置7 U. g. A$ h" R% t0 b1 M
进一步,再看看GPIO结构原理图,它支持4种GPIO输入模式(浮空输入、上拉输入、下拉输入、模拟输入)和4种GPIO输出模式(开漏输出、开漏复用输出、推挽输出、推挽复用输出)。因此才能通过多种输入输出模式组合来实现更复杂的其他外设功能。/ C, I }+ i+ [1 W
. M/ P# t& x6 N# Z4 |
2 \7 Q1 x' s1 s& _" k# ?8 }' L* M4 e* s* V# Z5 f! L
在cubeMX中在GPIO配置界面中,可以配置GPIO引脚的GPIO输入模式以及其他参数设置。 y$ `; t# e1 p- F
* g: ?9 _! ?2 ~2 a1 k- ]
' f7 w/ w H' a
: v1 X; M3 s, t1 m. ~
同样地,在cubeMX中在GPIO配置界面中,可以配置GPIO引脚的GPIO输出模式以及其他参数设置。
Y8 a1 D5 B! C5 m
- ~" c% X, H. N3 l+ {/ C
. ?& {6 {% V- {0 B4 Q8 K
5 M6 h: Z- Z# [( O" x N( r2 k 另外各种基于GPIO组合实现的外设,也可在GPIO栏的各个外设页面下,单独设置各个引脚的GPIO输入或输出模式。
6 e, X. z' g$ z1 R
4 M3 m* B4 T4 N8 u) x3 w% e7 d, ?
- w3 V9 w7 Z/ A" M/ a+ h, z% { |3 m4 v& H) _& Z' p! j2 D4 x
二、GPIO的HLA源码分析6 B& w1 z+ w6 W
2.1 HLA库GPIO初始化
! P, ^) a# s+ H6 @ 通过cubeMX生成输出源码,如果配置GPIO引脚功能,则cubeMX生成代码时,设置了为每个外设生成独立.h/.c文件时,会在Core源码目录下的Inc及Src目录,分别生成gpio.h和gpio.c驱动文件,否则将会GPOI相关输出函数放置main.h和main.c中。' w: T1 @8 a) U/ E7 X+ n- W
3 b v/ i3 C4 `- |. J$ x 在gpio.c文件中,主要定义了MX_GPIO_Init函数,该主要做三件事情,一是启动各个GPIO分组寄存器的时钟,二是对于输出GPIO引脚实现初始值写入,三是将在cubeMX上配置引脚参数写入到参数缓存区域,并依据缓存参数,调用HLA库的HAL_GPIO_Init来实现真正的初始化设定。如下面代码所示。
7 y, i* c% U5 `! }( i
0 l3 F' U: n! [0 j0 ] a- //main.h v6 i( k Z' G1 n L
- /* Private defines -----------------------------------------------------------*/5 t$ I7 R* K5 C* N0 O( i( |
- #define LED1_Pin GPIO_PIN_3
V) l, g0 Q8 S( G - #define LED1_GPIO_Port GPIOE
' m0 t j! T5 {; J - #define KEY2_Pin GPIO_PIN_10/ U: E6 A* s& y4 m, p1 f
- #define KEY2_GPIO_Port GPIOE% {1 f- Z: l5 ^" J5 c
- #define KEY0_Pin GPIO_PIN_118 g. ?1 E3 s) c: e
- #define KEY0_GPIO_Port GPIOE+ @7 ]# u" {! E: `+ t
- #define KEY1_Pin GPIO_PIN_14# d" d) y. M# z2 ]; A# Q* A
- #define KEY1_GPIO_Port GPIOE
! T1 `$ c7 f# J t - #define LED2_Pin GPIO_PIN_15. l- i5 b4 X3 O/ d" i4 @ _
- #define LED2_GPIO_Port GPIOD7 x* q3 X; U' Y) G! M
- #define LED0_Pin GPIO_PIN_6! S% Q. m7 E7 q( } h
- #define LED0_GPIO_Port GPIOB9 d _2 r+ ^) E
- /* USER CODE BEGIN Private defines */
3 P- d1 ^) g& ^" V - //gpio.c. K5 v2 D9 K* I M) F- n( D
- void MX_GPIO_Init(void)
5 }7 y+ \, {8 p w8 \+ e7 y - {
0 P& S# n9 o' N- ]* D" q- m -
+ E# B. m. \" L/ k/ ~$ @, A - GPIO_InitTypeDef GPIO_InitStruct = {0};
2 p+ V: x6 d' z( b8 Z c - * S* {8 n3 d: _. ^, p5 M; Q6 V
- /* GPIO Ports Clock Enable */
2 u( C$ P0 N4 U6 T: q5 c3 B, h - __HAL_RCC_GPIOE_CLK_ENABLE();- g0 H: d$ G$ R s8 v/ I! ~- ^
- __HAL_RCC_GPIOC_CLK_ENABLE();
8 y: Y* m% Q! x - __HAL_RCC_GPIOH_CLK_ENABLE();0 {: o6 O$ a0 j2 N5 D0 \
- __HAL_RCC_GPIOB_CLK_ENABLE();
+ Q4 X( x" k8 T8 L+ C" } - __HAL_RCC_GPIOD_CLK_ENABLE();
/ J$ C) `9 M1 k) Z% o' ]9 [ - __HAL_RCC_GPIOA_CLK_ENABLE();
2 ~# S9 q6 U! H' I4 K0 c -
- ^- g' R* a+ o4 v; ] - /*Configure GPIO pin Output Level */0 L0 d& T+ w9 J9 g9 l. g3 |
- HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, GPIO_PIN_RESET);
- x6 E7 q' J. ^1 g$ d% o6 D. m( O6 n -
. Y; K+ B' T$ T- A$ G( ]: j$ I* G - /*Configure GPIO pin Output Level */
- b1 r! c( L7 Y" f* e9 p3 S - HAL_GPIO_WritePin(LED2_GPIO_Port, LED2_Pin, GPIO_PIN_RESET);
/ f1 ~' M2 l! I9 Q6 [0 @ -
+ @8 k. j3 L: a2 o$ o& |( ?- v/ l - /*Configure GPIO pin Output Level */
3 @# q W8 m5 c9 A: W2 p: I - HAL_GPIO_WritePin(LED0_GPIO_Port, LED0_Pin, GPIO_PIN_RESET);4 Q, m* Y( c) u) G; O6 B% A: S
- ; A2 g- x- g6 V
- /*Configure GPIO pin : PtPin */1 m2 i, A" X8 \# x: ~
- GPIO_InitStruct.Pin = LED1_Pin;8 B5 Z. }2 B' F. j( z9 h6 a
- GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
+ V4 }6 j6 h, {$ _2 H - GPIO_InitStruct.Pull = GPIO_NOPULL;
9 V1 T" x) S* k' \) C9 C, c$ h - GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;# {: z* g. S+ W* O' z; k/ d
- HAL_GPIO_Init(LED1_GPIO_Port, &GPIO_InitStruct);
- { j) D. S( V -
- U0 s, p( l& T - /*Configure GPIO pins : PEPin PEPin PEPin */ [( l$ c* w/ q# ~' n& Z# u7 T! Z) w1 M
- GPIO_InitStruct.Pin = KEY2_Pin|KEY0_Pin|KEY1_Pin;, ^( S* y" b Q2 `" V
- GPIO_InitStruct.Mode = GPIO_MODE_INPUT;0 `6 }& |# [! r7 }$ ]
- GPIO_InitStruct.Pull = GPIO_NOPULL;9 z7 i" h/ u: r
- HAL_GPIO_Init(GPIOE, &GPIO_InitStruct);! k: J2 J$ @8 ?# X' ]
- 5 @/ S/ R/ c0 ^& L% O
- /*Configure GPIO pin : PtPin */
1 d( T% I! Y: x9 M- `' @ - GPIO_InitStruct.Pin = LED2_Pin;
0 W1 o; i/ B- q - GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;8 q/ i2 l s. [ ~, |: }4 ` H
- GPIO_InitStruct.Pull = GPIO_NOPULL;) ?- ~ x4 S/ x, g
- GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
. `9 @: D9 \2 i$ g - HAL_GPIO_Init(LED2_GPIO_Port, &GPIO_InitStruct);! a) X5 O/ h+ ^; p3 X' J) O {, {
-
3 Z! |9 Z% g9 U, A4 C, ?' ^ - /*Configure GPIO pin : PtPin */ H- |. }" ]% f
- GPIO_InitStruct.Pin = LED0_Pin;) n5 @5 \" q' E" W" f
- GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
/ n9 n7 s) S" u8 n6 ]6 w - GPIO_InitStruct.Pull = GPIO_NOPULL;# r: p. s* ^+ Y ?1 [8 m
- GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;- a/ Z- d$ J/ `
- HAL_GPIO_Init(LED0_GPIO_Port, &GPIO_InitStruct);
( c, r" e r- d0 Q -
. I. x- _/ |" ~. q2 K' n1 n - }
复制代码 & k- H) q/ n1 `, K0 m" D6 n* a
在stm32l4xx_hal_gpio.c源文件中定义了HAL_GPIO_Init函数,它做以下事情:1)诊断先前缓存配置参数是否合规,2)再将缓存配置参数写入到GPIO的寄存器内,完成GPIO的初始化设定(GPIO 的Mode、Pull、Analog、Alternate、Direction、EXTI等)。/ Z0 i& z7 r6 j1 I4 y( S* y$ i- {
2 ~+ G& {3 {( `$ u' _& T
, \5 D. Y: n0 x6 n
+ ^2 @$ a9 e' ?5 [ 2.2 GPIO的开发应用接口API简述
, D8 W& L& B' G. Y9 c/ X 除了提供GPIO初始化API外,STM32的HLA针对GPIO还外开发这提供如下API:3 p4 ]6 { \1 l8 e! r
9 S5 V B- @! U7 _; @8 I# N
/ L$ X' s% \. W9 S8 [( A6 `0 D: M, q3 Z: q" M, o# D, n0 U, G0 h
GPIO对数据的读取主要是HAL_GPIO_ReadPin函数,写入主要是HAL_GPIO_WritePin、HAL_GPIO_TogglePin函数,其参数都是GPIO_TypeDef* GPIOx和 uint16_t GPIO_Pin,前者就是前面参数的分组如GPIOA、GPIOB等,后者就是Pin0~Pin15,例如GPIO_PIN_3。
( s5 y( S/ S& o3 B" i. I# o& o; }3 u* u+ }" G5 J/ C
关于外部中断回调函数HAL_GPIO_EXTI_Callback,该函数是弱函数
5 J- k9 V. U3 Y5 A/ \5 R2 m
2 ^( T) p0 D4 e) r$ x) t
; Y( Y F0 s* n( }, J0 q
, K% i7 y' A4 c3 T8 [' d
若需要使用该函数,使其生效,首先需要在cubeMX配置界面,将GPIO引脚调整为GPIO_EXTI类型
: i3 q9 l K7 G/ P
^$ H# W- H, K1 b
3 J; B; T1 d1 r- c3 I7 z9 o; c% ]7 o l' s- G: l6 @- q _; Y8 ?8 W# M
并开启该引脚的中断支持9 k' y) L2 h" I ]( X* P
& N: f7 A8 N& |: i( A
) Y+ y( a* t* }. P" d! f- @) b8 n8 t3 c; \- A+ O C
就可以重新实现stm32l4xx_hal_gpio.c文件中的HAL_GPIO_EXTI_Callback函数来实现GPIO外部中断回调功能,例如设置如下,则实现按键KEY0按下异常,触发回调函数HAL_GPIO_EXTI_Callback调用,从而实现LED0灯状态反转(点亮或熄灭):
( W2 e3 e2 t# K: p/ j- /* USER CODE BEGIN 0 */2 p/ s6 d. L5 U0 f
- void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)! {: N; v" {1 g% x8 m+ j
- {
5 _7 |/ |$ e1 A0 f& H( x - if(GPIO_Pin==KEY0_Pin)$ ^: n2 s; B. [' h
- {
* Q: {3 ~) `' g - static uint32_t key0_count = 0;
6 t. J+ Y' J8 L' `0 `4 Z - printf("this is %lu count for HAL_GPIO_EXTI_Callback!\r\n",key0_count++);
e" R$ x2 s. H - Toggle_led0();//LED0等状态反转一次
7 \0 w/ R( g' j - }
3 g3 F( n" Z8 p - }
. R# @6 b6 o4 W. u - /* USER CODE END 0 */
复制代码
3 c. J( X' Y& a! l% n 同样地对于外部中断请求函数HAL_GPIO_EXTI_IRQHandler也可以类似地进行调整实现GPIO外部中断触发器调用,从而实现我们需要的业务应用。
. I7 h- H) h: w9 }1 a) l
. t. W6 o% u9 t/ z9 u GPIO的HLA库中,HAL_GPIO_LockPin函数,主要是操作lock寄存器数值的,lock寄存器会针对GPIOx_MODER, GPIOx_OTYPER, GPIOx_OSPEEDR等GPIO寄存器配置对应的状态标记,而其他寄存器操作时,会先去lock寄存器读取其使用状态来确定是否能进行相关操作,从而确保防止冲突及达成一致性。$ l! T9 \; {* H" @
————————————————( }6 C9 b7 J+ L9 {3 V
版权声明:py_free-物联智能
, E( b9 |# u# u% n5 F) |如有侵权请联系删除+ u0 S* `8 T. t; p1 @
|