12.1 关于按键
* A8 L7 B$ W( A1 \1 o前面控制LED灯是让GPIO输出高低电平,而获取按键则是读取GPIO电平,从而获知用户是否按下按键。# Z* g* c1 N4 y/ ?0 l. O/ a! L H
/ P0 A' U; s i" p$ X. y# B
按键监测一般有两种:按键扫描和按键中断。按键扫描是间隔很短时间反复查询GPIO状态,从而得知是否有按键动作,这种方式代码简单,但比较耗资源。按键中断而是通过按键产生中断信号,从而实现按键的检测,这种方式需要使用到中断机制,需要对MCU了解深入一点,效果是最好的。8 ~) |! U9 m3 o/ k
! q# s) I4 A9 ?; u+ `$ w# N2 k本节先介绍按键扫描,理解按键的基本原理,下一章再介绍按键中断,同时了解STM32F103的中断使用方法。
6 b, o/ Z0 F4 m; s% }9 B4 `: \5 t: v: ~" @* V: s- W
按键一般占用一个GPIO口,通过监测该GPIO的电平变化得知按键操作,典型的电路如图 12.1.1 所示。当所需按键比较多时,则可以采用矩阵按键减少GPIO的占用。矩阵按键需要通过编程扫描等方式实现对多个按键的监控,这里以最简单的独立按键为基础进行介绍。# w; H9 \; u5 v+ Y6 H
9 ^5 h3 T6 h1 Y2 U5 L
7 z: {. b9 X5 Z' z/ X ~0 u: Y图 12.1.1 按键与MCU的连接方式示意图
z/ l/ U+ Z* Z |可以看到,在没有按下按键时,电源3.3V通过电阻连接到MCU的PA0脚上,此时MCU读取PA0的电平就是3.3V的高电平。在按键按下时,电源3.3V经过电阻,再经过按键连接到了地,此时PA0连接到接地的一端,读到的电平就是0V的低电平。由此,MCU就可用过读取对应引脚的电平值,得知按键的变化。
) |' [# Y7 U: v, C5 b" U8 A" C- e; i5 M5 O# n2 z, |
常用的按键都是机械触点式按键,机械式按键在按下或释放的过程中,由于机械弹性作用的影响,会伴随机械抖动,如图 12.1.2 所示。# t8 l3 \2 U2 B. @) j8 L" S& J
+ x$ i' q! @7 T( o% V
0 m+ ]. `: Z' w# C7 K% _, d8 E! n) F5 @( N' y6 t0 m, F
图 12.1.2 机械按键抖动示意图
" L1 z1 v0 @7 a( d! A" h3 S4 G, l9 M: F7 { x8 O
S) v/ i% ?% S; U% ^1 s
抖动的时长与机械开关特性相关,一般为5-10ms。在这抖动过程中,会产生多次高低电平,导致被识别为多次按键操作。为了避免机械触点按键检测误判,必须消抖处理。按键消抖可以硬件上处理,即在按键旁并联电容,吸收抖动的电平。也可以软件处理,即通过延时,避开抖动。
8 ?5 T7 \9 o Y% G; a% L$ B
$ d. N: [! C0 A3 G e6 Z' \4 _由此,首先获取对应引脚的电平得知按键状态,再硬件或软件消除抖动。8 m: T5 H+ U% i0 L$ o# U- \
- ~+ l: c7 P3 q' b) K; M, N
: R5 `% J, I4 L1 l; f) A0 u& o# O/ j0 i, d7 ~- V1 o0 z
12.2 硬件设计( U8 X" p! e. I) M- e" m* L
开发板上有两个按键,一个是复位按键(K1,红色),另一个是用户按键(K2,白色),这里只有用户按键能够编程控制。
3 @" l& q. z/ N7 Q/ K4 e( D
9 O3 x4 ?* n3 ~3 u- L如图 12.2.1 为开发板用户按键部分的原理图,电容C15用于硬件去抖,可以看作不存在。按键松开时,3V3经过上拉电阻R20到网络标号KEY,网络标号KEY另一端连接MCU的PA0,此时读取PA0电平为高电平;按键按下时,3V3经过上拉电阻R20,再通过按键接地,此时读取PA0电平为低电平。
" A) t; @1 o( G- E0 C
& s e; m! C3 y5 z由此可知,按键按下,GPIO引脚电平变低,反之为高,按键所接GPIO为PA0。6 A# L: L8 Y3 o. v
4 i# X7 n$ I) ?, L8 D
( H! t) a& ~+ e! `( _9 _
" o$ a: e7 L8 a. [5 v, l4 [图 12.2.1 用户按键原理图
8 s' B, G0 I* w
+ q" F- W; r. H. g1 C# X. q! P4 p* G- \ Y# j
12.3 软件设计
8 V1 S4 i& c3 J( T+ v4 Y) F1.3.1 软件设计思路, x! b2 a @& \6 r
实验目的:本实验通过轮询读方式取GPIO的输入电平判断按键是否按下,并操作LED。- l6 q/ k2 u! ?
8 C U9 I' I. o2 W. ]/ P( Z7 ?
1) 按键初始化:GPIO端口时钟使能、GPIO引脚设置为输入(PA0);
4 E& s0 A8 i+ B. X9 g0 p; M4 U+ w! F( P
2) 封装每个按键处理函数:读取按键GPIO状态,操作LED灯亮灭;
# H" r4 [' K$ L" I/ ^/ n3 D$ N8 \! h! ~
3) 主函数轮询按键状态:一直检测是否有按键被按下;/ n' {5 w# i5 {9 r/ i7 O- R+ n% a
) E; H# p1 Z/ u; r
本实验配套代码位于“5_程序源码\5_GPIO—按键轮询\”。. X1 W2 U Q! {6 N
. \ l/ D* G% f8 Z2 P6 C
5 T. [/ G8 H' f( _7 n* U2 Q' @3 ~! A$ n5 {& P
1.3.2 软件设计讲解
4 w$ ~6 M+ F, _5 g' R( x/ s1) GPIO宏定义与接口宏定义
' A% v: |2 L, J" r' N" j
6 Y7 j: W% ~; n) |% ^; V) g- O代码段 12.3.1 引脚宏定义(driver_key.h)
6 {5 P5 p% [( J% v( H8 X
' q2 ], O& {6 U- /*********************
6 T' |" o6 e) n7 ]2 n
6 Z6 \2 J2 J$ w ^+ z o4 _- * 按键引脚状态定义
q% f% n. g# ? - 1 h# x4 L& U8 @% ~6 U' ?( _. f* m
- **********************/
7 O0 D; N# e. j( L% ^ - 9 p" M' G" a/ } A, o
- #define PUSH_DOWN GPIO_PIN_RESET4 l h( ?5 j- b
. _# _& m; x8 c) h# L% ^9 L- #define SPRING_UP GPIO_PIN_SET
7 J4 }6 S9 n f) p Z& @ - 7 V% Z/ A) Y" e/ z, ]0 C
- 3 M+ s T6 ^$ ]$ a: W8 B. N v+ O
- / w2 C& l" E9 j% P0 m2 n: @
- /*********************
1 m# T: q5 L+ A3 B/ ?1 D. C3 [) o - 8 K/ n, N% @4 R# S! f- _- W
- * 引脚宏定义
9 Y4 t6 ~! y9 m8 ~% G3 [1 _ - ' q) ]+ e+ b0 Z( C) t- V
- **********************/- b2 X K8 ]5 Q+ }) ~
- ) k3 O" k- h3 I$ d
- #define KEY_GPIO_PIN GPIO_PIN_0
& d6 \" e+ e7 e2 S0 i& H; w
4 P5 Y `; T9 f2 r1 u9 ~+ b- #define KEY_GPIO_PORT GPIOA) j% K6 y3 P- M
- - a$ q1 Q% Z! g6 b. R( [
- #define KEY_GPIO_CLK_EN() __HAL_RCC_GPIOA_CLK_ENABLE()$ f" {7 m( {" ?& J- ]
6 g% a! B& E6 f1 J7 Q. _+ }8 k2 a' f- 7 N$ _1 V; v$ ^" n: L0 {
- 8 @( J( j; c- J. k! _
- /********************** X; m, V- p! T9 C; [% [" J
# \9 Y) Y9 f6 Q% i: T5 R; J/ Q& ?- * 函数宏定义
7 o6 D" W0 x0 j' H3 d2 C: {# q - 4 n+ m$ [5 k1 O6 W
- **********************/
u' ?" d+ _$ W8 ]4 K3 _7 P) j3 L6 ` - 0 M$ A+ C L0 O6 d* K
- /*
. s! V. V7 s/ j2 z8 X7 D& X$ ]
1 S/ P7 z" P$ k& T( m$ O2 j- * 按键状态读取函数宏定义3 W1 r) Q$ U# ^! d7 I" T# \
% f6 g) {; ^" G+ z$ I( }- */0 J! f# W/ r' R3 C7 q
- 0 o4 u- E$ z# n* y& w) a. u
- #define KEY HAL_GPIO_ReadPin(KEY_GPIO_PORT, KEY_GPIO_PIN)
, n9 _: e7 g- b2 ]2 J3 K, U
复制代码 & d# [8 L$ P' O$ ^
( I6 w: z% v( b; \
根据硬件设计选定的对应按键的引脚,将其宏定义命名为KEY,且对他们的读取函数进行重命名。其中“HAL_GPIO_ReadPin()”原型“GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin)”,参数依次是:引脚组,引脚号,返回的是0(低电平)或1(高电平)。
! D2 ~" O" B% k
' W) Z6 R& z+ E8 ?% R) c- w! r% |! |2 E, I2 Y2 ]( I
" j9 F2 e3 d) t- y
2) GPIO初始化
& ]! Q, [1 J/ j0 I' g: d/ u
/ H' F& b) c0 n# I1 O代码段 12.3.2 按键初始化(driver_key.c)
7 t( d& A7 R* ~$ b7 H
- R# B% o# D w2 a- /*; c/ J0 S( |$ r& E5 _8 S" }
- $ Y5 l2 P2 O1 K% W2 s
- * 函数名:void KeyInit(void)
& r# P. B5 j- v. F - 5 K) v% n7 _* f1 I
- * 输入参数:无& t: P; o& M* ]: x$ p1 }0 Q* r4 A
- 9 S. @/ C8 f* }5 }; K/ J4 N. ]
- * 输出参数:无
# Q& D g( h8 m$ G* }* y, `3 N
2 j/ A* I4 n& I% l3 P- * 返回值:无$ u* r5 E& V) E: S) W* r- f
# q3 P6 x, l' A2 T- * 函数作用:初始化按键的引脚,配置为输入+ Z# Q& p: a$ y1 q
- v R8 X! X# a5 A: b$ _, Z$ |/ K- */
$ \7 ^+ [8 t5 {; R( n. Y
& y, D0 u- s9 {0 J# x5 X6 J% ]0 D- void KeyInit(void)) @2 z/ l* P8 @" q m0 h# c) {- F
9 `! P+ g! c# s* r& _- {
/ W+ K5 t/ w- J - : C' q5 r1 c; k* z: T, E: t
- // 定义GPIO的结构体变量) H" Y- P: }7 Z8 b; R( m$ Q
; L/ l" X8 m1 ?) k- GPIO_InitTypeDef GPIO_InitStruct = {0};
$ d4 z3 \1 H1 V @# l$ v$ \. P7 F
& M W$ y, w: [, n0 B9 p- // 使能按键的GPIO对应的时钟0 _# J1 Y! M& W9 e- n) Y& B+ M% y! Z
- : u8 E/ D I+ v0 q9 D# [& {
- KEY_GPIO_CLK_EN();
, L9 J' Y2 ]1 x# B2 b$ |# q5 n
7 w5 H% d) c* l7 O M+ ?- 6 s1 h+ R4 j! z5 X9 |1 ~6 J
- / i9 r9 R5 ]6 U( F1 O( _
- GPIO_InitStruct.Mode = GPIO_MODE_INPUT; // 设置为输入模式
n6 Y z7 C9 C% p, L+ ?6 [ - 0 y0 D) ^6 |8 S# T( K5 a
- GPIO_InitStruct.Pull = GPIO_PULLUP; // 默认上拉
1 J' F& A' @6 M. v! s - 9 _, G" ?' [2 p
- GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; // 引脚反转速度设置为快* u2 K5 L5 a: c6 k& [4 @
- ( {0 X0 N4 c+ b& |" c; @
; a& G1 m+ T( E4 a3 M# B* j- : C4 h; x! V- K
- // 初始化按键引脚配置8 `' g) s0 S5 K8 d/ U a
- 7 r: q6 h0 Q: N, p8 X- r
- GPIO_InitStruct.Pin = KEY_GPIO_PIN; // 选择按键的引脚
' V' j5 G+ K6 A6 R; E
3 e$ t; {/ I5 u) f1 h _7 _- HAL_GPIO_Init(KEY_GPIO_PORT, &GPIO_InitStruct);
( [' z5 K4 w. l% z+ \ - ; O3 n- } n% z: N3 @ i2 O! L- e
- }
复制代码 ; U! z H# \; x
使能时钟,将引脚初始化为上拉输入。
8 L Q# K) A/ {; p( U3 ^6 g! J# I9 k: A0 x* `; E1 G% I
% {2 J: \' s( r6 j& \5 N$ @
# z* x- A/ Y+ a+ t+ H1 O3 C! D
3) 按键读取函数( c4 s9 E( n7 q, s- D, S( R* ]0 X6 n
; t7 B3 Q, ?, H0 x" S L
为了展示效果,编写代码查询按键是否按下,按下后切换LED灯亮灭状态。) \$ c7 A5 K: A" M: B' s- y3 k
8 x+ a* {- t4 L z6 T" Q) t
代码段 12.3.3 按键读取函数(driver_key.c)
( h3 P: U: Z% K
* M1 ~2 ?+ M/ t! ~- /*
, I0 Q0 c6 a# ? - 3 V( S s. ~* b) {
- * 函数名:void KeyPolling(void)7 v3 ?: i" e. M- j L0 b
$ W* B' Q0 m% Q7 h! \4 @- A9 D) `) @- * 输入参数:无' `* s- z0 u- r
- & Z( u0 P( F0 V# F! {! {+ A8 p
- * 输出参数:无3 Q$ h4 } D. h& s
- ' i: n, {2 ~0 g& E- i- @0 g8 n
- * 返回值:无4 ]+ m5 C, V$ Y) ?( o
- 6 \, j6 F4 U- A2 J6 H: E4 z8 E
- * 函数作用:使用轮询方式查询按键是否按下,通过按下控制LED灯亮灭
/ |; t3 ^3 V( ]! }( x+ h- V3 c
6 N5 R3 s9 h- O4 q- */6 a6 ]+ A0 H! x
/ |- ~ p: R1 N1 W# \3 U9 i- static bool key_flag = false;
. X9 Z: n# G' U" H) L
/ i: d' d4 l0 T. D" B+ I$ n0 O) f$ t- void KeyPolling(void): ^: y( w( m9 E3 S* p
+ [1 u! A. P$ T% X5 x- B- {4 S, W7 B4 i; e4 L; c
0 f+ H' _, c# I6 b6 r- if(KEY == PUSH_DOWN) // 如果检测到按键被按下
# Z+ g, f/ t' [. ~6 ?
/ H6 |2 c2 _3 X( C6 h- {0 I& ~6 S7 n5 e, J# D6 f# N- j
7 M( G: K0 Y* t% H; M+ z- HAL_Delay(8); // 延时8ms防按键抖动, x, X. }& m* l q3 W$ h
1 O) X) U( i& P5 ?2 p- if(KEY == PUSH_DOWN) // 如果防抖动后按键依然是处于被按下的状态,就认为按键被按下过9 h5 B5 K# \: Y; X5 z
- 3 y1 E7 e8 }5 j
- {
$ l J! Y9 @. I' B0 g4 f - 8 F N0 U3 l: V0 |) b
- key_flag = !key_flag; // 用一个标志位来判断按键被按下次数,按下一次灯亮,再按一次灯灭,如此反复; l6 ]/ @- i) b: ?4 e4 t
- 2 P$ s* b: b+ w0 v5 T
- BLED(key_flag?OFF:ON);
' Q+ U$ \" D% I% f/ W' F
) N( l) i$ K) y7 i& d- }* o+ p/ r, A7 x! H9 r
. Y; s* ]+ ~3 h. X% V- }2 F3 g' R$ z" `2 |6 A3 p+ h
/ _, B; \% }2 i8 p2 C' P+ M1 C- }
复制代码
- K; l x6 ^! {% ]- |/ ~! V8行:定义了一个全局变量标志位“key_flag”,作为按键被按下的标志;" z: t6 y4 m+ g6 I
! O, O o" h) Q$ n+ r. w& a8 S
11行:获取该按键状态;
0 Y; o# \. T% X2 z! L! b- d8 P. N. q0 X! N7 {- ]
13行:延时5-10ms,软件去抖; {$ D. M4 G! @7 |
% U1 {4 ~: }9 ~
14行:再次获取该按键状态,此时依旧按下,说明是正常按键操作,非抖动;" W: Z, ], ~, R
$ @0 E: q" W! t) B16行:将标志位置反,按键按一次置反一次(即0->1->0->1这样循环);5 G" f- Q7 l0 l- e
6 K+ l# a' q5 o8 ]3 u1 `
17行:根据标志位“key_flag”的值,让LED灯亮或灭;
6 a0 @5 t. J* l, j1 w2 W+ ]
8 V& k3 O& ]1 e7 D此时每按下一次按键键,蓝色LED灯将亮灭交替。5 p+ \% [0 x" E: N( y
3 z$ k$ q1 O$ f7 }# U' B: B- g9 E6 G1 v: J: a" O
" u# a) p2 Z, U2 v4) 主函数测试
7 R6 s7 G/ d4 I
1 W+ a& u0 Z1 c/ ~- @' S代码段 12.3.4 主函数控制逻辑(main.c)
3 w; m* Q% O3 n3 U/ S; k8 @+ L5 q# K8 ]. v
- // 初始化LED0 M" I, l- `9 N: m
- - P& i% i& n2 X
- LedGpioInit();
* B" K, g) R- {/ j+ f! \ - ) W5 p, M# w+ R1 a7 l) H" ]
- // 初始化按键
' H( t# d- Q9 a/ D8 T, D - 6 f9 b# U: O2 y1 h2 k
- KeyInit();
2 M. T0 G; w+ v8 U. P
1 S# ?( ?1 @' J9 `- 6 l$ `& ^6 S# D9 A$ o) F% j& I
- O0 Z1 c; \( P
- while(1)
1 l/ [0 a; Q( p2 O/ G8 r5 g
4 c- C9 i/ P/ X& G- {
1 T, K* `1 N ]/ q* S7 \5 M
" w6 ~' S" Q3 O; ]* `& |- // 轮询按键键! h+ ]. B" f( R; v4 y
( O. V9 I, }3 N3 y% L# U- KeyPolling();* ~2 U" y) ^8 h0 G7 z) }( B
- 6 `% W7 |! i$ z. B
- }
复制代码
# Q' f* X9 _2 x1~4行:初始化LED灯和按键;' Q l) K) G- j8 _" i+ _; z
0 g9 D, v/ N! D( o, v6~10行:一直循环查询每个按键当前状态,从而判断对应按键是否按下;% Q. v- V! [: h$ }5 j
+ B8 I+ o* q. U7 o12.4 实验效果) ~ _$ ~1 R3 t8 C6 x4 i4 L; v! N$ s
本实验对应配套资料的“5_程序源码\5_GPIO—按键轮询\”。打开工程后,编译,下载。按下按键,用户LED灯亮/灭。
$ N; y0 U) v) ]( P! W9 W
" C g9 e: p" d3 W9 U作者:攻城狮子黄
0 e7 k m5 N" M; l( W( N, b
( M. T4 Z/ B. u g o0 S# t* N d3 f& T( n7 T( ]. w
|