一、什么是GPIO
/ R* `7 y- `; {1 J3 W2 _GPIO(英语:General-purpose input/output),通用型之输入输出的简称,可以用来输入高低电平或者输出高低电平。这里的高电平指的是3.3V,低电平指的是0V。通常称GPIO为IO口,或者引脚。STM32F103ZET6有GPIOx_0~GPIOx_15,其中x = A,B,C,D,E,F,G。8 v% [3 V, j9 z& i1 l. A
5 a m& l8 ]6 e6 x3 c. z D
二、GPIO的输入/输出模式
6 M% v3 X5 D4 F% p" j9 ?6 @: VGPIO有多种输入输出模式,输入模式有
! S: x) o1 A+ l7 P5 H* G3 m6 w' R# `• 输入浮空 输入浮空指的是GPIO与外设之间既不接高电平,也不接低电平,呈高阻态。除了类似于在数据传输时将GPIO配置为输入浮空外,一般不配置为该模式。因为输入浮空状态的GPIO电压具有不确定性,可能是0V,也可能是VCC,或者是介于0V和VCC之间的某一个值。 \0 \# Y% ^, D' t3 E
• 输入上拉 输入上拉是通过一个上拉电阻,将GPIO拉至高电平状态,不受外接电路的影响。
0 ^4 s' u& q% U• 输入下拉 输入下拉是通过一个下拉电阻,将GPIO拉至低电平,不受外接电路的影响。, N/ M6 ^& h5 K k2 Q7 X+ C- D
• 模拟输入 模拟输入模式通常用在ADC采集,采集模拟信号。$ ^. \1 ~( Y7 L3 S" {3 |. g
+ i. A/ w, s6 l$ f, @& i+ n
输出模式有
/ U7 t/ |, L" U* b l; |3 C: U• 开漏输出+ L3 b1 m. w% S) _8 m& d
• 推挽式输出
& E5 L' e0 g9 h2 j/ S* g• 推挽式复用功能
) M# r4 }9 j2 S+ z• 开漏复用功能
6 @$ b- }* ]8 n; ~0 t! @' ?& i n
! }% B4 [' i+ k+ f对于一些输出模式这里就不再做详细介绍了,贴一篇大佬的文章深刻理解GPIO。这些模式在接下来的学习过程中会慢慢的介绍这些模式需要在什么时候使用,这里只需要知道就够了。
/ l. y/ b: X# h# [5 u3 v7 L0 z6 h% J) `+ a. |
3 [; T/ W' `2 ]% K; H1 g( f P3 w
三、GPIO初始化配置
5 T! _" P& ]& ~1 _; h: W本专栏介绍的是使用库函数进行开发,很多内容都是库函数提供的,相对来讲非常方便。在初始化GPIO时有一个结构体,只需要对这个结构体进行配置即可。结构体中包括想要配置的GPIO引脚,GPIO速度,GPIO工作模式。( x9 ], c i2 X
8 D W! r. K0 W7 ]. k+ J8 {初始化GPIO的步骤主要有
, n# N ]% _7 @' W5 x• 定义GPIO结构体
% S# k/ }) q; R j6 p: d• 开启时钟 GPIO工作需要提供时钟信号,在初始化结构体之前需要将时钟打开% {# ^ R5 W* y
• 配置结构体成员 GPIO_Pin是想要配置的IO,GPIO_Speed,通常写GPIO_Speed_50MHz,GPIO_Mode是IO的工作模式* e" }& j H( ~2 e! \- k
• 写入配置3 `6 d& j0 N3 E% ?. [4 D0 {1 x
4 G2 E8 P, N j5 |, @
GPIO的工作模式在程序中有定义
2 k; [3 w+ L, G5 p; ~2 ]- typedef enum
# {, J, |; I% `* j @0 \4 w - { GPIO_Mode_AIN = 0x0, // 模拟输入
B4 G: Q6 t1 t( m - GPIO_Mode_IN_FLOATING = 0x04, // 输入浮空
1 v5 h( h D2 D4 c3 E* a - GPIO_Mode_IPD = 0x28, // 输入下拉
& }- |& g4 Q% d. S$ E7 Y/ H - GPIO_Mode_IPU = 0x48, // 输入上拉
2 f( G" M2 y( k+ j6 W4 |2 W% y - GPIO_Mode_Out_OD = 0x14, // 开漏输出
2 J! F( b( c# w! C) m1 E4 a4 ` - GPIO_Mode_Out_PP = 0x10, // 推挽式输出
+ G3 h" n. D- f - GPIO_Mode_AF_OD = 0x1C, // 开漏复用功能
- }& H+ Z# p$ q' K7 N) k" e# W - GPIO_Mode_AF_PP = 0x18 // 推挽式复用功能
* b3 f/ l$ g/ s - }GPIOMode_TypeDef;
复制代码 * b# y& t1 x; d4 V
初始化GPIO的例程如下9 S* L$ O& g2 H
- void Drv_Gpio_Init (void)# I) }1 X& h: X+ J! I+ S: F+ d
- {
6 ^% C% K6 ~8 k( G5 g6 B/ ? - GPIO_InitTypeDef GPIO_InitStructure; // 定义结构体
7 G% {1 B. o9 D4 s3 S0 G% ` - // 开启时钟
) O) `6 r2 [7 E0 y8 ~& r8 [) y - RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB,ENABLE);; x& d9 L8 T$ {
- 1 E$ \* F4 N8 F0 i
0 k8 R: z# g. d a2 u2 c i0 k- // 配置结构体 D+ a9 r1 B1 }. w; ?
- GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_8;
' o+ P. ^) z, H* F, ]4 Y - GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
, ]: _& d9 _/ r& ?+ k+ e3 r% C9 f - GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD;
, J3 \' o. N8 s% _1 n9 y - GPIO_Init(GPIOA, &GPIO_InitStructure);
- [; Q' F8 m/ T8 P -
8 _% n3 F) q& N! L, K8 P - // 配置结构体
. t9 M1 J0 W3 M$ M- H2 K - GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_2;
; \; d# B+ M/ }7 W* x. ^ D6 O9 v - GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;: Q$ h" q5 r: ], K% o% L
- GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD;) E4 R* l& N( r% t* d$ J6 m
- GPIO_Init(GPIOB, &GPIO_InitStructure);
& a6 ^% p2 ]# H - }
复制代码 8 Q: B7 h1 e4 T7 Z$ i
这里初始化的是PA0,PA8和PB1,PB2。在开启时钟不必要写两遍相同的代码用来初始化GPIOA和GPIOB的时钟。写一句,用“ | ”同时开启两个GPIO的时钟。写Pin时也同理。7 A* R b! l5 ~) c' w& O
* H: F- y, A# m; H四、Boot引脚; }/ v. e4 a5 F0 U
这里简单介绍一下Boot引脚的配置,对于只是利用核心板编写程序的小伙伴来说Boot引脚的存在感较低,但是当我们需要绘制硬件电路图时,Boot引脚怎么连接就显得很重要。中文参考手册中介绍如下
8 |' Z. q6 v p7 `* I8 F" g2 G9 T3 ?) [! s4 i
- |' |) y$ D( }# t; H& E" M中文参考手册Boot引脚介绍
0 [9 |7 ~: z' P7 z& a/ }+ z @
y/ ]# o: a* p0 O& J
2 Q3 r- r/ _2 {2 t0 S: a! c2 d这里简单说一下本人经历得来的经验。如果想使用USB转TTL通过串口下载程序,Boot0需要通过KΩ级电阻接VCC,Boot1接地。下载完程序后再将Boot0接地。需要注意的是这个KΩ级别的电阻最好用10KΩ左右电阻,如果电阻太大会导致下载失败。
" y) r# `/ r: ` K5 s
_/ }9 p. {; I2 g1 ~0 V' c/ m4 L由于本人在使用自制板时只使用过串口下载,所以对于用调试器下载需要怎么配置Boot引脚,未测试。
# j) W+ {, s8 l6 u0 X( O6 L6 D5 B: S% M: d
2 u8 H( Y) ^9 E6 _五、一些特殊的GPIO; X- `2 f* K" ?# E
在使用GPIO时需要注意一些特殊的GPIO,否则你会疑惑,为什么一些引脚的高低电平无法控制。其实有些GPIO在上电后就有自己的默认设置,会稳定在高电平或者低电平。比如用作JTAG的几个GPIO——PB3,PB4,PA13,PA14和PA15。这几个GPIO在上电后就已经默认用作JTAG,即使用上面的GPIO初始化程序将这几个引脚初始化后,依旧无法控制(无法正常用程序拉高拉低)。
! t4 q1 G/ ?$ G; t" t
* P1 U Q7 `% r1 d) d这个时候我们需要对这几个GPIO进行复用重映射,关掉其默认的功能,才能作为普通的GPIO使用。针对JTAG/SWD复用功能重映射,中文参考手册描述如下3 ?+ A" ]! r& M8 L
& Q* I+ p. V8 c/ R
3 K* z. n" ?" k& |; Y2 U# L5 }* N中文参考手册关于JTAG/SWD引脚的描述
2 l- d* ]& J7 Y9 ~* u% M( ^; Z) `3 r; [" h5 e+ k0 y
j1 d8 \$ B6 M/ @& |) r图中说明这几个GPIO有几种模式,完全SWJ,表中的五个IO均不可作为普通IO使用。完全SWJ但是没有JNTRST,PB3和PB4可用,依此解读。上图是针对寄存器开发描述的内容,使用库函数开发时有封装好的函数,但是需要注意的是需要提前开启AFIO时钟(对于AFIO是指什么,大家可以自行了解)。使用库函数开发时针对特殊引脚进行重映射的操作步骤为
6 ~+ c9 i6 B9 q0 s
; G9 ~+ S' r ]4 m1 ~• 定义GPIO结构体
9 o8 s( c3 W! y# G% }& o k2 K8 }• 开启GPIO时钟和AFIO时钟* r4 `. B2 Z7 X/ Q; O' K& E$ k5 R9 [
• 重映射引脚(根据所需情况设定引脚模式)
+ M, o( l/ Y& p; I• 配置GPIO结构体初始化GPIO
" ]; d. c. M' ^6 D! [& u; f8 ]5 H0 ~$ Q3 _1 c1 _ F
库函数中提供了可以选择的三种引脚模式! f0 F$ G3 }! N, P% K3 m, ~
- GPIO_Remap_SWJ_NoJTRST // 完全SWJ(恢复引脚的默认功能)
; H1 J! ?4 d" v: H" ? - GPIO_Remap_SWJ_JTAGDisable // 关闭JTAG,启用SW-DP
! l' _4 F8 J8 o+ U) u - GPIO_Remap_SWJ_Disable // 关闭JTAG-DP,关闭SW-DP
复制代码
3 t: N* h; a [' d- s
6 D+ _; S. w1 o! M& u$ L2 T提供了一个函数,可以进行重映射操作
7 I' I9 v9 y* H5 [0 \- p& e0 b- void GPIO_PinRemapConfig(uint32_t GPIO_Remap, FunctionalState NewState);
复制代码
8 I+ \- D- {) b7 k8 V当需要使用到上述的五个GPIO时,初始化程序需要修改,举例如下7 v9 ?) b. E: U. }
- void Drv_Gpio_Init (void)
& N% v3 A. _ B- I, i% N# }4 x - {) Q9 g( j9 e q3 }
- GPIO_InitTypeDef GPIO_InitStructure; // 定义结构体
( t2 j5 W5 r1 e - // 开启时钟6 |9 h5 J% v" t( l- D! a
- RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB |
; |; e& ?7 `7 ^& s ^( D - RCC_APB2Periph_AFIO,ENABLE);$ Z/ b+ J5 S* n& d5 X R
- ) t1 J& X- ~5 q7 I
- 7 k7 ]3 u q$ Y4 [
- // 关闭JTAG-DP,关闭SW-DP
2 @# }, Y; U# O; P+ k. G. Y: H; N - GPIO_PinRemapConfig(GPIO_Remap_SWJ_Disable,ENABLE);
9 N i+ Z% A3 N' d -
3 L# H) m- x( U" q- H - // 配置结构体
% @8 q0 t9 }, r% ]- J3 i" ?+ R - GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15;
+ R4 Q$ A# e A - GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
0 O0 }/ ^+ r) t, c$ r6 p - GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD;; N- ]- i5 i; z2 Q( P, e
- GPIO_Init(GPIOA, &GPIO_InitStructure);% L2 M: n, @, {1 l2 e4 n
- : w2 n ]6 B3 [( r
- // 配置结构体7 X! L2 h) V/ W T. u/ E8 s
- GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3 | GPIO_Pin_3;) g8 M* O4 x' ^* n% P9 T
- GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
+ ~; v+ G0 B1 P* d8 J4 r - GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD;8 }6 q$ U+ p9 H) r- d
- GPIO_Init(GPIOB, &GPIO_InitStructure);
' w4 p. Z1 |3 O, a4 t) { - }
复制代码 3 x. n" D2 R, I$ t. I
STM32F103ZET6芯片GPIO资源较为丰富,平时开发时尽量不要使用这几个特殊引脚。
. s' y8 o3 Q; E) h# p* X" o3 w/ v( q
! p! E. [: v9 N8 I7 _
六、点亮LED1 ^/ D* S3 H. h! M4 p2 h
学习完GPIO后我们就可以利用GPIO进行一些简单的操作,比如点亮LED,使用蜂鸣器,用IO驱动小黄电机,检测按键等。9 q( K+ g9 g8 o4 }- Q" z3 ?
" k6 \4 h; D/ a- W! [; `5 Y6 \' Z
点亮LED比较简单,但是也有一些需要注意的点8 B9 T! b' N1 H v# E3 W
% |: N! l" n. [$ O( A8 V+ W/ z4 K' i
• 根据硬件电路确定LED是高电平点亮还是低电平点亮3 i4 e8 N! b. B$ n! [" h" A( {
• 初始化GPIO后要先将所有的LED
/ K( `+ v# F, h/ h% _9 O0 t0 Y1 K2 I+ \3 r( f8 A- z
6 s% J' |6 _( w6 ?, p# ^' B6.1 硬件电路6 e$ u. V7 w; Y% C0 E% C& M4 d
下图所示电路中,LED右侧接3.3V,左侧接IO口。此时如果想点亮LED,需要将对应的IO电平拉低。相反如果右侧接的是地,如果想要点亮LED,需要将对应IO电平拉高。7 Z7 x' X5 k( F" _0 ^
/ K# V' L5 O/ @1 X# M! ?* p2 f$ ~5 ^3 M- i
LED电路 ; E) D3 |' o: f1 W2 D& g9 D" M
9 B+ ^+ K: j: [( d4 v
% N3 Q- n6 {; A, X! i
6.2 拉高/拉低GPIO
: ^9 i: ]' e% o5 f! N9 \2 S如何将GPIO电平拉高拉低?库函数提供了封装好的函数
" n: u( o9 A( ?7 J, c K, ?" x: Z- // 设置为高电平+ W/ V( K7 q8 S& j! W1 j
- void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
6 i& k9 i/ _. {7 {5 Y" T
* d' [' @$ N3 f" p
- u; `7 C @8 t& P5 f: w- // 设置为低电平
! C2 V# z" A, P# L7 | - void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
复制代码
( N! o9 M1 i; s t. F U v8 Z6.3 程序设计
0 S5 _, x0 j4 A, g0 ^点亮LED很简单,只需要初始化相应的GPIO,输入模式设置为推挽式输出,然后设定电平即可。
5 _2 t* E: O. E. B! h6 q7 c3 h6 u$ n" a3 n6 C# a; C$ Y
这里给出点亮LED的例程,LED电路为一侧接3.3V,另一侧接GPIO。" h, u. b' r& f8 ]( ^: A3 V0 G
( Q6 V5 k$ U/ F, J* A3 N这里给出的只是一些必要的函数,如果需要工程模板可私信联系。
7 w, V( B& N9 ]# O9 b
: [$ j* [; F: x需要注意的是,初始化GPIO时需要开启时钟,开启时钟之前需要确认GPIO挂载的总线。5 Y+ c+ z+ F, I$ N$ i4 [: }
- /*( [! p% T: E- R& @; X
- *==============================================================================/ m( E9 Q( B: O i2 C2 U7 Y, s
- *函数名称:Drv_Led_Init
' I# D1 ], c, _. k, }) B+ @2 b - *函数功能:初始化LED的GPIO
7 W( @7 a- g6 m4 \& s8 ? - *输入参数:无8 G, S+ t: w' N) U/ A$ |
- *返回值:无
, W$ J& U: P. c( @6 X* R - *==============================================================================: g' |) b3 |$ M) s* w+ B' c* g
- */
/ w; v1 M6 o- M8 }2 T4 [7 f3 n - void Drv_Led_Init (void)
2 z |, O4 D! C) p# c$ {4 H2 X; ] - {
d' z0 U$ }: P4 w {8 f; Z2 J# p - GPIO_InitTypeDef GPIO_InitStructure; // 定义结构体
. h! s( @3 P. ]2 S/ N - // 开启时钟
; h. y7 R3 B5 c! ~7 c - RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
" e; t4 F4 k9 v# ]4 G% k - 0 w$ x b* P, X, S8 b: ?
, g8 F+ d" X9 O% L7 d/ }- // 配置结构体
0 T" b: c0 W- ^+ F! F3 y& W8 u/ H - GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;
& C) e$ R* l" I: F1 I$ E, l - GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;1 n7 I/ o' _1 Y5 w2 s3 ^9 t1 e# ^
- GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
6 {" R' M* W; L/ { - GPIO_Init(GPIOA, &GPIO_InitStructure);) l5 [( o$ N. @0 G+ h4 e: r
w8 B! f# U4 T0 u: K7 U# e' o& M- ) f' w" w1 B2 ]6 z+ H
- GPIO_SetBits(GPIOA,GPIO_Pin_1); // 熄灭LED- Y/ p# \) p1 {
- }1 f$ v1 E9 C- g: E3 f/ x4 d2 p. }
- 6 i# Q6 \; O& L& A* _: z, ^# B
- 8 F: I/ F1 Y8 P1 w, Q) i
- /*
, |3 V* d, U8 q. h( m( }; o - *==============================================================================/ v, X2 c H7 ^ X; d% b
- *函数名称:Med_Led_On
; F% } b$ Y# h# T% x - *函数功能:点亮LED
8 H* Y+ J3 a- O- {' o - *输入参数:无, |9 N. f. q5 Z5 c5 n- z4 f
- *返回值:无
: y6 F$ A8 a( r1 }: s, m, R. D - 备注:如果有多个LED可以定义一个结构体或者使用swtich,增加一个输入变量3 p" ~7 H$ W' R7 R' \
- 来确定开启哪个LED6 p/ x/ n& p; F# c
- *==============================================================================$ D B' y3 b) R: k4 G# b
- */
5 d f# m5 p9 U0 P4 q! n7 \2 ]9 l6 b - void Med_Led_On (void)- u; g& I; [' f* r
- {9 c: x6 N3 l" |/ f V
- GPIO_ResetBits(GPIOA,GPIO_Pin_1); // 熄灭LED
6 e. j, |; F! U { - }
复制代码
- F- H, G' A7 m' _0 a# f七、GPIO的位带操作
& C& \, \- G: {% u对于什么是“位带”这里就不做解释了,大家可以自行搜索。这里只介绍如何使用。位带操作可以使操作GPIO变得更加简单。在模板程序的sys.h文件中已经定义好了各个IO的位带操作所需内容
% K' o5 T8 H) d/ f+ j z4 {4 {& Y" I- //IO口操作,只对单一的IO口!* X' @. [" [2 H+ ^
- //确保n的值小于16!, a; b9 A! w0 B: [# J" N
- #define PAout(n) BIT_ADDR(GPIOA_ODR_Addr,n) //输出
+ m- W' K9 [4 h6 x$ M( b - #define PAin(n) BIT_ADDR(GPIOA_IDR_Addr,n) //输入 ( B. ^3 i. `5 h+ P; C
. l4 A- a5 e: {0 Z" F5 V- #define PBout(n) BIT_ADDR(GPIOB_ODR_Addr,n) //输出 + s _: u- x& b, {
- #define PBin(n) BIT_ADDR(GPIOB_IDR_Addr,n) //输入 y7 Y/ X3 \- e) s8 s
+ I/ Q; Q# ^* l- #define PCout(n) BIT_ADDR(GPIOC_ODR_Addr,n) //输出
: C& E; m& k3 a3 N- c! V - #define PCin(n) BIT_ADDR(GPIOC_IDR_Addr,n) //输入
5 d" f6 C6 x" O, r5 d
7 m* K8 E/ m. `9 H5 T$ w9 W+ D+ J7 C- #define PDout(n) BIT_ADDR(GPIOD_ODR_Addr,n) //输出 ( e2 i2 K, K1 I$ z& @" Z8 K1 y% x8 G
- #define PDin(n) BIT_ADDR(GPIOD_IDR_Addr,n) //输入
- _9 R5 H0 r8 @( d0 W8 v) Y
" q6 Z$ G; a( @: X! z5 d% ^- #define PEout(n) BIT_ADDR(GPIOE_ODR_Addr,n) //输出
- L) ^3 M( A- D3 v& r- J' T. Z1 e - #define PEin(n) BIT_ADDR(GPIOE_IDR_Addr,n) //输入
) j, C- L+ I8 H& b" A
8 j2 \, S& P9 m, d- #define PFout(n) BIT_ADDR(GPIOF_ODR_Addr,n) //输出 8 n6 f$ J5 T# {7 H% H- U/ Q
- #define PFin(n) BIT_ADDR(GPIOF_IDR_Addr,n) //输入& N5 K. L3 l+ c+ D% D6 f. J
- 3 r1 T9 K+ F; L% d k1 v
- #define PGout(n) BIT_ADDR(GPIOG_ODR_Addr,n) //输出 . _( ^' k( E7 S8 p( }+ z: b1 c
- #define PGin(n) BIT_ADDR(GPIOG_IDR_Addr,n) //输入
复制代码 ! B/ Q7 T, p" m+ F( t6 n
有了位带操作后,就不需要再使用前面介绍的拉高拉低函数来操作GPIO。比如需要拉d低PA18 c2 ]! F" T0 a0 ^
& t3 a: k& T+ H* D4 m
有了位带操作后也可以对GPIO进行宏定义,用自己想要的名字来操作它。还是拿上面的拉低PA1举例9 a" @6 D% j% X1 b6 N* _
- #define LED PAout(1) // 将PA1宏定义为LED
( V, K9 q# e! j
. L1 ~2 ~; `9 |. o2 t' r8 y- LED = 1; // 拉高PA1
) n; R& o- q; b) d! ^* V* x3 K& f - LED = 0; // 拉低PA1
复制代码
0 S( Z; {2 B( Z: i4 ?0 g* E
6 @3 G/ ~: u: X! k转载自: 二土电子
: d2 v* J/ Q ~3 k; o- o$ O如有侵权请联系删除* [6 ~- {- Q# v) ^$ }: V
+ p5 P% l: Z, H, a( d( Y
& H+ o9 H2 L1 e* J4 k, y5 H' e |