12.1 关于按键
2 @) U+ u0 C( e$ r H0 N, c前面控制LED灯是让GPIO输出高低电平,而获取按键则是读取GPIO电平,从而获知用户是否按下按键。0 C% L k' a6 y: Q) |* D
0 n @! w* w% v3 D% @" b0 q按键监测一般有两种:按键扫描和按键中断。按键扫描是间隔很短时间反复查询GPIO状态,从而得知是否有按键动作,这种方式代码简单,但比较耗资源。按键中断而是通过按键产生中断信号,从而实现按键的检测,这种方式需要使用到中断机制,需要对MCU了解深入一点,效果是最好的。' Q4 C. [' P4 C3 `
5 n! j7 D3 Z- q4 w2 v! X本节先介绍按键扫描,理解按键的基本原理,下一章再介绍按键中断,同时了解STM32F103的中断使用方法。
" B4 o g6 q& B9 c! Z' h1 [: [* c/ P; q/ }1 ^
按键一般占用一个GPIO口,通过监测该GPIO的电平变化得知按键操作,典型的电路如图 12.1.1 所示。当所需按键比较多时,则可以采用矩阵按键减少GPIO的占用。矩阵按键需要通过编程扫描等方式实现对多个按键的监控,这里以最简单的独立按键为基础进行介绍。
% d6 g) y: z( J% s& C; o9 d
7 T( B* @6 R% n, I7 G% x3 L
6 @2 n; w6 a" t: v
- U0 H3 c' e4 F7 i图 12.1.1 按键与MCU的连接方式示意图- @& u6 D# Q( Z( A5 n
可以看到,在没有按下按键时,电源3.3V通过电阻连接到MCU的PA0脚上,此时MCU读取PA0的电平就是3.3V的高电平。在按键按下时,电源3.3V经过电阻,再经过按键连接到了地,此时PA0连接到接地的一端,读到的电平就是0V的低电平。由此,MCU就可用过读取对应引脚的电平值,得知按键的变化。
4 J# X5 W4 ~3 W. z: Y+ F
3 i9 p2 n+ ^" a( C2 d1 J常用的按键都是机械触点式按键,机械式按键在按下或释放的过程中,由于机械弹性作用的影响,会伴随机械抖动,如图 12.1.2 所示。, `" i+ m8 A. B$ q u! u9 T
/ I( b! |# D8 _5 ^8 U m+ R' b4 J3 R
9 L* }* c3 B7 V. B
~: L- W9 H I. R5 J9 D1 w2 ^1 @3 i* A, V) y9 `1 I
图 12.1.2 机械按键抖动示意图8 t- n$ v a" u$ K) t/ {4 v( Y
0 W! X$ o' `" G1 Q
* z0 @/ p3 ^7 s; `1 I抖动的时长与机械开关特性相关,一般为5-10ms。在这抖动过程中,会产生多次高低电平,导致被识别为多次按键操作。为了避免机械触点按键检测误判,必须消抖处理。按键消抖可以硬件上处理,即在按键旁并联电容,吸收抖动的电平。也可以软件处理,即通过延时,避开抖动。/ w, _/ i: s( w7 O/ q+ h7 k! V
: @7 x5 Z( M. @: {
由此,首先获取对应引脚的电平得知按键状态,再硬件或软件消除抖动。
6 [1 S( V' G- Z, ^+ y6 s6 y8 s5 D8 Y! F1 v
8 ~# ~( t- {% S, u
12.2 硬件设计
0 a3 V' R4 f. Z8 c2 ^6 q开发板上有两个按键,一个是复位按键(K1,红色),另一个是用户按键(K2,白色),这里只有用户按键能够编程控制。/ U0 N! f! U: [3 K
" x4 Y4 v7 Z/ u& q
如图 12.2.1 为开发板用户按键部分的原理图,电容C15用于硬件去抖,可以看作不存在。按键松开时,3V3经过上拉电阻R20到网络标号KEY,网络标号KEY另一端连接MCU的PA0,此时读取PA0电平为高电平;按键按下时,3V3经过上拉电阻R20,再通过按键接地,此时读取PA0电平为低电平。4 M) M! a# ~! ]. Z, y
$ D0 S% |) V& n
由此可知,按键按下,GPIO引脚电平变低,反之为高,按键所接GPIO为PA0。! @) G: C0 V* l2 i- G B1 P% ]4 u+ P
, F3 j+ m/ K6 x
' k* @9 f# w2 u
* N% i% [" A2 T: q8 `1 z4 f图 12.2.1 用户按键原理图: W" w3 M0 D! p y
2 Y7 c9 j7 M4 T3 U9 I8 {/ H: k4 y4 Q+ o
: z- @( ?. V' ?$ K12.3 软件设计9 D7 K5 b# f+ m$ C/ h: n) _
1.3.1 软件设计思路
2 |0 [, B' V& ~ S实验目的:本实验通过轮询读方式取GPIO的输入电平判断按键是否按下,并操作LED。
. Y8 Q' c* Z8 A' s7 f; Q& o0 ^1 `, x" z) W) K
1) 按键初始化:GPIO端口时钟使能、GPIO引脚设置为输入(PA0);7 Z0 X c+ ] B! b2 c" u
/ U9 r# g9 O0 c, v2) 封装每个按键处理函数:读取按键GPIO状态,操作LED灯亮灭; ?" W) x# o) i$ o l* S0 R
% W$ H' z7 k+ w4 \3) 主函数轮询按键状态:一直检测是否有按键被按下;
3 ?& q( v. S) y' j h: t6 e
3 g1 S8 P5 a) S+ I0 u本实验配套代码位于“5_程序源码\5_GPIO—按键轮询\”。) m; x* R( l# v3 K
' D( {6 u v! `
& [/ c, ]' Y" u; T' Y4 A' B$ v, W1.3.2 软件设计讲解" }4 I3 d* U( e: z2 O
1) GPIO宏定义与接口宏定义
7 a( ?. @: b: Z6 Q) [: s' U7 N0 b
* [- F8 H$ Z* c代码段 12.3.1 引脚宏定义(driver_key.h)
! @ i% K5 l' e& g! H# N5 ?% Q1 W: g: L: r! E7 c4 ^
- /*********************
; w+ m+ W/ c$ J
' d% i( R1 w9 r5 }- * 按键引脚状态定义# E- a: ^2 O+ g
/ L# G) {0 A5 e+ W$ @8 `% U- **********************/
0 m4 L) g2 d8 o; X7 W( T
% ]1 g0 J2 F7 E0 [% y- #define PUSH_DOWN GPIO_PIN_RESET' x5 w5 u! P) A! ^3 k: ?2 f
- / S \+ `; }, U
- #define SPRING_UP GPIO_PIN_SET
& J# o2 U& c3 J; N& x
# c9 r8 e6 E3 M2 c
/ G' i+ g% F9 }" |3 ~- + w r/ T2 B* y: ~
- /*********************4 e' A g& s; U) I) E! v L7 K% T
- ' O2 c$ I# n9 F( S# o- g; u: z
- * 引脚宏定义/ u0 r. P6 ?5 B4 a+ ]0 t2 L9 T" p7 ?/ v
& \7 p8 B: ]2 [6 P, u+ P8 {- **********************/
0 i. n% x& t/ n4 ?" W - 3 q6 f. x6 o7 }& m6 L9 G$ a
- #define KEY_GPIO_PIN GPIO_PIN_09 ^& h9 s4 ?& `% k1 B4 k
- 1 q, O( [# L& X0 s! w8 U
- #define KEY_GPIO_PORT GPIOA
T$ X4 H- K9 b( Q& K1 I: P, B- \
5 B* `4 P- |; a; V- #define KEY_GPIO_CLK_EN() __HAL_RCC_GPIOA_CLK_ENABLE()
' T a* e! q7 j9 j, J7 \: d
# ^# }3 @* P+ ?0 J1 E
9 c6 U9 }( s6 Z: }& S- 0 A8 H# D1 c+ w, t
- /*********************
% C- Q/ `. ]' X0 f7 |& M - 9 X! p* ~$ u) N) o( U
- * 函数宏定义7 W- V+ T. Y! ?9 v* {, {3 V
- 1 j% q2 j8 ?- ~5 _
- **********************/
! w. C- N1 {. c. Y) P2 ]8 U- `3 {! M
0 N5 L5 R6 V% z l- /*
/ {2 ~5 `3 p" g* x7 Z6 y- G
! Q6 {' }+ i: m; M( ?- * 按键状态读取函数宏定义# [2 ]: j. \% U% I
) n" ^2 N) X8 ~- e1 W- */ y( e* Y# N" i& U5 Y
- 5 p4 n& d& |; \
- #define KEY HAL_GPIO_ReadPin(KEY_GPIO_PORT, KEY_GPIO_PIN)
复制代码
) t' X% u3 a$ j8 k8 I8 a/ r7 I1 y* V$ b0 i1 r" M# t
根据硬件设计选定的对应按键的引脚,将其宏定义命名为KEY,且对他们的读取函数进行重命名。其中“HAL_GPIO_ReadPin()”原型“GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin)”,参数依次是:引脚组,引脚号,返回的是0(低电平)或1(高电平)。
8 R4 U8 r) Q% N9 c$ k& ]! C5 h7 G) J6 q3 X% b: W& [
N9 t# s7 J' }, F( g% M' F# I Y
2) GPIO初始化 }, M- H8 L1 Q' L: K# C
$ W8 w0 l1 ^; ^$ Q代码段 12.3.2 按键初始化(driver_key.c)9 y$ c; g# u0 u0 r% Y8 g# m7 D- |
# j* h W9 m7 j( v0 L8 h) L- /*
9 M' h% l1 I& u7 R4 m( U - 9 f/ G9 {) I* y$ X
- * 函数名:void KeyInit(void)
7 T+ [, N- U7 x/ l. Q# x; u O
, F3 C0 F% O! P+ z% c0 U5 ^- * 输入参数:无1 O: v. G. t& Z% x: {
- ' |4 Y+ ]! u2 H' E% n- S; _1 K
- * 输出参数:无
( G4 u# n% a$ t3 `9 |! {
+ w/ F* Q7 v' F' |4 ^5 G2 u/ {! v- * 返回值:无' y* a' w- X5 Y! f5 i5 K+ X
8 l) V; g$ z! ~: x0 E- * 函数作用:初始化按键的引脚,配置为输入, l% J( T; j! Y( _
- 3 G0 P* w) X0 R! W# f2 M/ G8 H
- */
& `, u3 L! K- F8 u" v
: z) X3 ` }& d% c- void KeyInit(void)
+ R& y+ v. [( A/ p$ C - , t, d# a3 K9 a5 P
- {' R! e" G- \, d/ I$ s: J( q
- ' k2 w* F9 z O$ U! I* P
- // 定义GPIO的结构体变量
0 G. C [" |; ^3 A7 r! i* f& _1 n. P7 y - ) j- s6 v" D( H) m. g
- GPIO_InitTypeDef GPIO_InitStruct = {0};
6 i) T5 ]9 k t6 K+ s2 g: E - 2 q8 b7 b- l# t- F8 |# Q
- // 使能按键的GPIO对应的时钟
% ]: K4 I/ ^+ p1 F
9 G/ {5 L) y x+ e- KEY_GPIO_CLK_EN();. l6 x f) G- o9 k# m3 x
- ! T" ]3 f# |/ ?# z$ a
- ( r& u6 \# g/ r$ x& p* _) h6 c1 `
0 p5 Y- Y+ m9 T" h- GPIO_InitStruct.Mode = GPIO_MODE_INPUT; // 设置为输入模式/ W0 u, \9 _; c/ Y% {5 X
- ( n T+ Q4 I3 v" f6 h3 _
- GPIO_InitStruct.Pull = GPIO_PULLUP; // 默认上拉
- Z: _/ x7 Q5 Q. W2 s3 d8 q: Z, u - , z& c8 Y1 I: J0 T( g. x3 m
- GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; // 引脚反转速度设置为快# Q; A5 Z4 r& }& i. |
- 6 V0 y+ W6 A R0 ^
- & J: E/ h! K+ x. G5 V
- b0 B3 D( l2 @% ^9 a* b( ~7 K
- // 初始化按键引脚配置
9 f, p* \; z4 C" J# K9 k' v - 7 X: g, z- ]7 R' e
- GPIO_InitStruct.Pin = KEY_GPIO_PIN; // 选择按键的引脚
7 y. Q- e# ~# q f7 D% C* m - 0 j8 Y8 D: m& o- v
- HAL_GPIO_Init(KEY_GPIO_PORT, &GPIO_InitStruct);
+ A6 U" K9 M& i M8 i9 T1 C. m8 S - # H* m8 G/ A2 Q7 w4 [
- }
复制代码
' |8 Q( X: V H7 @) {8 L* d5 U' R. v7 y使能时钟,将引脚初始化为上拉输入。$ F; ^4 A9 o5 I0 B2 }% w. ^, v
! [ q- |; E. J5 o9 N
- V7 T7 U/ o3 R' [7 G3) 按键读取函数0 Z$ a) [5 H6 M" N( M
: `9 I1 D$ `1 m0 Y4 d; [1 x; M$ O为了展示效果,编写代码查询按键是否按下,按下后切换LED灯亮灭状态。9 {, N6 P+ T" R" N7 P
: |4 N. ?* A7 U/ }* y
代码段 12.3.3 按键读取函数(driver_key.c)" f2 Z" t/ b* r! ]- w9 I
& Q" R% W/ u0 W r- /*- G4 @" m$ Z, F. S% x
- & r" Q5 J8 b" R' O" v
- * 函数名:void KeyPolling(void)0 \3 C" `+ K; v/ a+ |) O
5 u9 v# a* f" F6 c* E5 j3 b- * 输入参数:无 `/ e! ?' U7 L4 M3 D
- ( h% K" t0 Z4 I1 O" R
- * 输出参数:无' [1 m2 f" y! ]1 T m, B9 J
- $ z1 ]5 L0 v' z+ G
- * 返回值:无
. [$ n, x, r: q- ^8 _, y - 0 y0 w# |$ u% d. Z3 E. N
- * 函数作用:使用轮询方式查询按键是否按下,通过按下控制LED灯亮灭
1 U8 d$ Y# | S0 X. ?
- g3 v& L/ C6 G, D6 h. y* U6 O! r- */0 k4 h9 t# f0 V3 _" z U
- ! ]$ p6 A p% y; b
- static bool key_flag = false;/ v7 p% U# }: q# N
+ Q" {" ?$ W& \* Y2 R- void KeyPolling(void)- {# m$ s* l; E# S* n
- + m4 s3 R0 y, i4 H$ K
- {3 @% y; l6 z' a2 y3 ?0 t3 g; n
1 f, A: @! q% h, l. K7 T r4 B! }% K- if(KEY == PUSH_DOWN) // 如果检测到按键被按下7 T) U* h$ G p I- Q
- " O* f& i/ _% s- j' q
- {1 ?- F4 r- [2 @0 `
- ! D. R# } c+ K; P# ~( `
- HAL_Delay(8); // 延时8ms防按键抖动) Z; [$ d: p" n- [3 x
- / [/ _" y: ^7 {3 {1 k( p
- if(KEY == PUSH_DOWN) // 如果防抖动后按键依然是处于被按下的状态,就认为按键被按下过
! R" H/ ~' Q/ W) a
5 i* {; @( w) O N% [* p( p- {7 _% e8 f/ H, v$ A S8 P
- / e6 z5 X5 S0 ? K* b
- key_flag = !key_flag; // 用一个标志位来判断按键被按下次数,按下一次灯亮,再按一次灯灭,如此反复4 x9 G" Q9 n n1 H8 \* E, c( G
- ( ^, h# i1 L8 c/ A0 b6 u9 D$ q4 V
- BLED(key_flag?OFF:ON);
7 p; u* [% ?- H" f' w j7 Q - . I5 f3 J5 H+ z- U$ x" r
- }8 f7 Q* M* z! R" o) D5 R
- . f* q' V$ ]: \7 e6 K4 D2 Y* \0 t
- }& W. A6 |6 d, K; q
1 P$ \/ y/ @$ a7 p& C9 q- }
复制代码 * Q: C6 U& H, B# G
8行:定义了一个全局变量标志位“key_flag”,作为按键被按下的标志;
9 U; W/ d" Z" _4 _0 k1 d
& c0 L( {# Y, c8 a11行:获取该按键状态;) @. m! Z6 d/ I% |, d; O/ p2 d
9 h. W- O$ i1 P- a: O& T
13行:延时5-10ms,软件去抖;
8 }; J) k( U/ r. s8 r& U- U( y- v2 E5 Q& ]* q# B0 i' d
14行:再次获取该按键状态,此时依旧按下,说明是正常按键操作,非抖动;
1 \1 y1 o# D/ \# O6 J0 k" i( x/ i9 [3 d' [3 X+ y ]% O$ ^/ ~; F
16行:将标志位置反,按键按一次置反一次(即0->1->0->1这样循环);
( p: T+ S: ?5 d/ }
0 N: T) y' y: s; ?17行:根据标志位“key_flag”的值,让LED灯亮或灭;3 T, Q0 Z0 q# B6 F
. F# o3 \( S' G% X, ?此时每按下一次按键键,蓝色LED灯将亮灭交替。
1 I ~/ m7 }" k( y
+ h7 G& Q4 x1 r t( h$ [* p
1 Q9 j* L/ A: ^' G3 L4) 主函数测试; Q- ^. W# Y$ }4 @3 p$ Y7 y% i
3 {3 Z. ^- }; K+ a( i+ i
代码段 12.3.4 主函数控制逻辑(main.c)
" p. q/ r' ?; E; Q8 {
' I0 P' E3 E& q- // 初始化LED7 V9 u# Z7 o- l; p
% Q2 Z7 f9 X+ |7 m0 }4 G- LedGpioInit();
: X) ~! \' C1 J o - # W+ s; m( u+ t* l% _1 S) s1 h
- // 初始化按键
+ T. b+ H h5 y5 X3 S
3 `, r$ h! |+ i/ e$ z- KeyInit();
/ ]( k" [+ X! [+ C: e - " ~& V# i: M% @$ _
- " M: L. ?6 r& t& {' \: |* z
7 J* \% X& L; h1 @" n B( d- while(1)) N4 X! v3 {! U7 J
' t$ j' G( Z0 k5 M- {5 n6 m0 U& z) X3 r2 V0 C" K6 R2 k
+ u6 l: \/ \ d% z- // 轮询按键键
f, i- P# `) n4 K- z' ]+ F" u - 4 \9 c: N8 y* l
- KeyPolling();0 W2 I/ \" n9 _
9 M& _+ _/ P8 j4 ~- }
复制代码 ( b2 q) r% d* @8 I: m; y/ v3 n
1~4行:初始化LED灯和按键;" ? i P, O _, W8 t% I& H
9 w# n+ z0 j1 ~2 | B6~10行:一直循环查询每个按键当前状态,从而判断对应按键是否按下;4 I i! i7 y- }$ S
% p0 i& _; X: b, I9 S
12.4 实验效果
0 ^9 y$ l) M* v" H4 h本实验对应配套资料的“5_程序源码\5_GPIO—按键轮询\”。打开工程后,编译,下载。按下按键,用户LED灯亮/灭。
+ d; w' f! g% H7 J# [% k/ L" p$ Q( j% F3 T
8 k- C* @8 ]- W$ e7 \ |