12.1 关于按键! H8 v" V. p! y' |
前面控制LED灯是让GPIO输出高低电平,而获取按键则是读取GPIO电平,从而获知用户是否按下按键。
0 N: \- \/ |1 L, U; v K, T2 a7 ]- a& p
按键监测一般有两种:按键扫描和按键中断。按键扫描是间隔很短时间反复查询GPIO状态,从而得知是否有按键动作,这种方式代码简单,但比较耗资源。按键中断而是通过按键产生中断信号,从而实现按键的检测,这种方式需要使用到中断机制,需要对MCU了解深入一点,效果是最好的。
& c" c1 I# I- @% o& L+ t- N2 n* `9 Q9 t, f: s2 N6 ~' _
本节先介绍按键扫描,理解按键的基本原理,下一章再介绍按键中断,同时了解STM32F103的中断使用方法。/ l; x+ ]: A: u0 [* K! T
{, p, T+ F: T/ |: L: E2 D' E2 s
按键一般占用一个GPIO口,通过监测该GPIO的电平变化得知按键操作,典型的电路如图 12.1.1 所示。当所需按键比较多时,则可以采用矩阵按键减少GPIO的占用。矩阵按键需要通过编程扫描等方式实现对多个按键的监控,这里以最简单的独立按键为基础进行介绍。7 j! u9 u. _" Y* t7 m
6 H3 x v2 G, u( t8 I) J+ h" ]9 D( z I9 I
图 12.1.1 按键与MCU的连接方式示意图
* B n. b. e- e/ Y8 a7 ~可以看到,在没有按下按键时,电源3.3V通过电阻连接到MCU的PA0脚上,此时MCU读取PA0的电平就是3.3V的高电平。在按键按下时,电源3.3V经过电阻,再经过按键连接到了地,此时PA0连接到接地的一端,读到的电平就是0V的低电平。由此,MCU就可用过读取对应引脚的电平值,得知按键的变化。9 \" y, e/ ~+ B" O* S8 @, E2 G
% e6 I9 N3 ?( S9 m, ?
常用的按键都是机械触点式按键,机械式按键在按下或释放的过程中,由于机械弹性作用的影响,会伴随机械抖动,如图 12.1.2 所示。
8 d: f7 h3 T: b6 g* a7 f. ~/ E" h1 |5 [! N0 i
6 V6 L( j* V9 W0 a4 U3 Q
; |" M% v8 T. Y& [: _/ g% k图 12.1.2 机械按键抖动示意图
5 W2 x( Y9 `+ g% G$ g- ^9 W, M( B. D( `: M1 }7 b& d
e, b% u' d9 J, B4 d
抖动的时长与机械开关特性相关,一般为5-10ms。在这抖动过程中,会产生多次高低电平,导致被识别为多次按键操作。为了避免机械触点按键检测误判,必须消抖处理。按键消抖可以硬件上处理,即在按键旁并联电容,吸收抖动的电平。也可以软件处理,即通过延时,避开抖动。
# a+ M. ]! p3 u; [5 G/ O" A" H4 P
: q' _( I4 X3 B由此,首先获取对应引脚的电平得知按键状态,再硬件或软件消除抖动。
) t( B5 D+ V, M( Z. ~8 [) j
# i4 T2 \; L. k* f) O5 O; A+ U, c- G& F) }
( T3 e1 ~$ `+ T4 D1 m( I ~- d
12.2 硬件设计5 s4 @/ `2 s. @+ n0 r* {; i2 }
开发板上有两个按键,一个是复位按键(K1,红色),另一个是用户按键(K2,白色),这里只有用户按键能够编程控制。
; b) b2 l6 H( e d7 d
7 s0 P6 h* ~6 [ b! {2 y; n' M. l如图 12.2.1 为开发板用户按键部分的原理图,电容C15用于硬件去抖,可以看作不存在。按键松开时,3V3经过上拉电阻R20到网络标号KEY,网络标号KEY另一端连接MCU的PA0,此时读取PA0电平为高电平;按键按下时,3V3经过上拉电阻R20,再通过按键接地,此时读取PA0电平为低电平。! F# d& R, [4 z
0 ?: H1 @( S: [. k7 S% V
由此可知,按键按下,GPIO引脚电平变低,反之为高,按键所接GPIO为PA0。
- J1 g: x; P+ O) z# D0 D; K
4 r( u/ K; }2 T0 R: h" C. ^
2 Y$ p, m c. U9 p2 j2 ]; ^1 O7 ^( J. q, ~* }+ u
图 12.2.1 用户按键原理图 - L5 f2 q% E, ^1 G2 x
+ d! b! G; l" ~/ s" D" s
1 \% h, S/ P8 o6 R5 I; j0 H12.3 软件设计3 p5 u% V e4 Y2 N4 {9 Y6 B9 {
1.3.1 软件设计思路
( ~1 s3 I4 C$ m" O& Z实验目的:本实验通过轮询读方式取GPIO的输入电平判断按键是否按下,并操作LED。$ O4 s9 [$ g& J+ N# _, N; l
4 r3 _7 Z/ J& K) x1) 按键初始化:GPIO端口时钟使能、GPIO引脚设置为输入(PA0);8 F4 N/ G4 T" k! l) F
; b) v# K/ k3 L( R' j' y; t9 W2) 封装每个按键处理函数:读取按键GPIO状态,操作LED灯亮灭;
& B. q4 ^5 c, `. ~) U( N2 \# b
+ K0 u: }$ a# ]/ [5 ?2 H3) 主函数轮询按键状态:一直检测是否有按键被按下;
% B- m9 m2 o3 i E$ H3 J/ T& q, p' V( \ l ^; u6 q- V
本实验配套代码位于“5_程序源码\5_GPIO—按键轮询\”。2 c) y4 t& v. h1 m1 t# F/ v
1 |: b; `, ?8 t; ^# u
$ I3 j# _3 H; ~8 B- {. h8 c2 J9 H& ~, h) m
1.3.2 软件设计讲解
1 g7 @( ]: L- X1) GPIO宏定义与接口宏定义3 F* d4 |6 G" i. F3 a1 e$ {. w
( O# h m* ~4 r$ d
代码段 12.3.1 引脚宏定义(driver_key.h)8 @' _8 y# B( q& J; Y9 m% d
$ j3 o' Q8 p4 C1 O/ {7 o- /*********************
% L- S4 e+ z T# a8 K# Q
/ c& |& ^# i! w( n3 B8 _& a- * 按键引脚状态定义
! k0 a7 ?& V! r2 i/ A* Z& H
+ B% K6 G# P6 R- **********************/
% j$ l; H0 Z5 l: B! m, Y2 ` - / p( |) k C) ~5 N% i F
- #define PUSH_DOWN GPIO_PIN_RESET* g0 ]. O8 M0 y& L& G; Q: i% T
8 G. H \& f4 K: J* V h3 M+ i3 k- #define SPRING_UP GPIO_PIN_SET7 g# C" C$ u, A! L5 l! i5 o% {
- 8 p* P; m+ l: t& R. {6 o
- 5 W) |4 l, J' g8 n
- 3 ^9 F5 y" F, \: |0 r* S
- /*********************! H0 W! X5 u: V) t
& o0 ~* ^1 D" V. P& q. f# u) l8 D- * 引脚宏定义
5 k/ z4 b* U0 S# J& ]) _" y - 9 H9 x; P# |+ _1 d$ Y, e
- **********************/& [2 U0 a3 b9 b; P& ~
5 [9 {% v# V+ g- E$ Y1 v- #define KEY_GPIO_PIN GPIO_PIN_0
: K6 G; o R# g; {- b D4 [, W - K7 l* O. A- x/ L" w* Q
- #define KEY_GPIO_PORT GPIOA
. k: a& n/ u, t1 U$ _
. L9 U2 F) M" K- #define KEY_GPIO_CLK_EN() __HAL_RCC_GPIOA_CLK_ENABLE()1 D9 k, G3 _+ g" r: e
7 X8 o/ ~5 Y, s6 f5 }! s
+ k- C6 Y1 u- |$ b. N$ v5 f. A8 e- ; n% q" f; z; X9 F& b2 @- n
- /*********************5 {' b/ H( e; \* z( }
, J" [9 c% s9 e" @' D- * 函数宏定义5 Y- W; X' m D! E0 {
- ' m1 a& x! ?8 P* f
- **********************/
R. m) |. G0 y) k3 B' G - 2 x1 ?/ w5 `6 v5 p& E$ q
- /*
0 T j) s3 D) c, d - % M% o9 S# A! C6 c9 I
- * 按键状态读取函数宏定义
* L/ K! X; D1 _6 ]" |. g* T
" L' M2 i" z) O* Q+ |% L3 G- */
- ~: n" `4 l5 }$ s# _, w+ K - , _ k& ]( N+ N4 K# ~ Z' A
- #define KEY HAL_GPIO_ReadPin(KEY_GPIO_PORT, KEY_GPIO_PIN)
+ m2 I, L: O3 @' }( m
复制代码
6 R, r; g* q( O6 ?- y4 _+ k1 m- |
( m& V2 q4 ?* ~5 R% }# i, X" n根据硬件设计选定的对应按键的引脚,将其宏定义命名为KEY,且对他们的读取函数进行重命名。其中“HAL_GPIO_ReadPin()”原型“GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin)”,参数依次是:引脚组,引脚号,返回的是0(低电平)或1(高电平)。9 [$ Y0 S6 j3 j& t& [" H
, H y6 a) J' A7 E) i) v
. G7 b3 S4 E* f7 T" R3 w2 \; m+ _2 H6 s0 Q7 ?" ^
2) GPIO初始化/ S y; K4 k5 S: y! O+ F. H
4 q: Y |& D/ ~, d. E+ @6 J! c代码段 12.3.2 按键初始化(driver_key.c)
5 Y) S M' L/ Q8 R
" _; [0 n8 E e8 p9 V- /*
1 N( f) D1 L5 d1 ~; o
% y$ D" J4 z, y+ M( H- * 函数名:void KeyInit(void)
+ E) z2 m/ ]9 ^( ~6 z7 j1 k' V - ( |9 {: T& `8 n0 A# B; p
- * 输入参数:无
& L- m) S c7 R. R; h, o
5 z: b# p& \ B' F6 u2 x8 A9 r1 ?- v- * 输出参数:无3 d( J; B; T$ w( k! ]; n
; c0 M3 T1 a6 k3 M% L8 l/ c7 x- * 返回值:无
( L- |7 E& Z Y2 v' ~0 J - 3 U: a1 Q% w) v+ W! {' Z% g
- * 函数作用:初始化按键的引脚,配置为输入) M. o$ Y3 b |
% g3 R+ p6 [; F! C- */, }; v8 @6 Y" s
- . H' t4 d) T: l& ~1 W2 ~
- void KeyInit(void)
) ?3 i6 l$ G! ^7 R4 [% j& S4 d
5 k# U# ^; }" W w( C9 Q- {
" z- q' B+ d# i1 D2 k+ t3 l3 I
8 I: U% @; y; a$ j- // 定义GPIO的结构体变量7 g9 _' E1 z" S! h3 f
- % f4 Z! y: I, X6 ]
- GPIO_InitTypeDef GPIO_InitStruct = {0};; `3 z1 ?. v- ~7 C7 W! L
- ' B* b+ ^/ G3 C. |/ ^! S0 y
- // 使能按键的GPIO对应的时钟
8 N+ c' }' Q# I) J0 j- c - : f. C7 S& C$ g$ o2 w3 i$ V
- KEY_GPIO_CLK_EN();5 h+ ]& I$ }" \: i; z
. w0 i' o! k3 n: A3 F- - }% A6 H- f2 ?' V! i# a
1 i% Y& ~7 N; M* I' Y! b5 ~# J- d- GPIO_InitStruct.Mode = GPIO_MODE_INPUT; // 设置为输入模式3 @) u0 [9 F/ y" v% |9 ]8 I: g6 R o
w+ |& A. X0 t' U- GPIO_InitStruct.Pull = GPIO_PULLUP; // 默认上拉
8 j2 P/ m! M: J' j8 W - 5 B k+ x1 |8 U1 E2 Y: k! \" r6 Y" G( f
- GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; // 引脚反转速度设置为快
2 S) G! l: I: O$ `5 c/ N( p - 0 \/ Y/ ?8 U: i- z2 A' f% x
; K* W" ^3 A# L- 4 X! L8 g! N. [) q
- // 初始化按键引脚配置
' w0 r6 k/ F; t3 y; O) @
3 Z8 u# _, m5 ]" j( _- GPIO_InitStruct.Pin = KEY_GPIO_PIN; // 选择按键的引脚
3 M) s0 e9 h3 p. V( X7 t3 p
/ [- n0 P2 s. K! O/ b- HAL_GPIO_Init(KEY_GPIO_PORT, &GPIO_InitStruct);" [( t8 t) _6 B! |
0 T7 @" p4 U% J- }
复制代码
# y* f; y, Q% H% I- M使能时钟,将引脚初始化为上拉输入。2 t! B7 e- e; g- T
. q* Y6 S; p& S7 W3 u% ~/ l N+ b! G
) [1 y5 ]* @$ b6 h# v8 N# m: n. z' X
3) 按键读取函数: J# Z! D* C3 V: N. P1 M
& [8 g. R3 E2 E; J8 q9 G5 I! t为了展示效果,编写代码查询按键是否按下,按下后切换LED灯亮灭状态。+ V1 `. s5 c2 d, k0 W6 p
5 P; \: l/ s) g. Y
代码段 12.3.3 按键读取函数(driver_key.c)) a& P; B4 g' _; N
, v( t- {: Y% P) C1 n
- /*
$ w9 K2 L' C& `/ r0 l2 M - 3 o0 V+ E3 k' i; ]" M
- * 函数名:void KeyPolling(void)/ O8 c v4 x, i$ v7 D8 c
- q# \( p' z1 k$ K- * 输入参数:无0 O7 j& w. w, o X3 E$ t3 }& B0 S9 B
8 G8 P( c0 V# R9 ^) x* I; o- * 输出参数:无5 c; E5 h( L/ K7 c! P, T' c. [$ D
- $ o0 i. p2 ?; a- t- g
- * 返回值:无/ T1 I Z0 X+ I" Q: H4 u$ }" e
! F s* J9 g& S, {1 k' D) h- * 函数作用:使用轮询方式查询按键是否按下,通过按下控制LED灯亮灭$ N8 G/ d( s8 C1 `/ o; w; P+ v; E" q
- 8 X+ n3 u, ?: W
- */
- h# w4 T5 K5 f) n: M4 J - 6 H3 C* y0 O. k- Q2 o
- static bool key_flag = false;: z3 P. z4 L: O' L/ Q. Z
1 b+ e( l( W0 R- void KeyPolling(void)
2 t ~5 Y' K7 |: Z( e r
7 a+ K! F+ c0 E0 e( f* {6 a7 V- {- j0 d- |' }6 }: [8 E/ V
* C# J2 H/ p1 j- if(KEY == PUSH_DOWN) // 如果检测到按键被按下
5 ?; L) e; s% J1 z& \ - : _+ T2 J9 c: W
- {
% i9 O/ K# r+ S - $ A; l( ^! E3 n: X8 B% K% [
- HAL_Delay(8); // 延时8ms防按键抖动% q$ Y+ t5 u8 O j
- % J7 F' } T( V, k C
- if(KEY == PUSH_DOWN) // 如果防抖动后按键依然是处于被按下的状态,就认为按键被按下过
9 m; C8 {( g* Z: A3 X
4 x8 V9 M6 ^: f$ n/ }) G9 y4 a- {
9 O# a$ z f$ n: `+ j - N/ l6 K( I! o! u! W0 \ v' E D
- key_flag = !key_flag; // 用一个标志位来判断按键被按下次数,按下一次灯亮,再按一次灯灭,如此反复7 A9 R2 t* C5 f, i2 m0 B0 z
- ; B: q }# Z" ^
- BLED(key_flag?OFF:ON);. g% Y! _8 g4 H
6 @8 E! S$ o3 {5 U* n3 w6 N- }
" {$ v% Q9 H- e1 L9 p
8 m( X6 {( ? d' Z$ q- }- K1 T) B1 K: U4 E
" G$ f9 ~) t; w6 w1 @& i$ M- }
复制代码
2 M' {, \6 S% |: q8行:定义了一个全局变量标志位“key_flag”,作为按键被按下的标志;
5 w z8 n, P1 q& A. Z0 X+ M8 d+ k/ a3 T+ o+ o5 ^" W6 @5 o- c$ n- h
11行:获取该按键状态;3 m* j, p: l0 d! F3 }" I# S- D( t
( g" R3 b- _; ~( w+ u
13行:延时5-10ms,软件去抖;+ J* N) K3 M1 }" j# P: H W
1 }0 }5 u6 K) p14行:再次获取该按键状态,此时依旧按下,说明是正常按键操作,非抖动;& x( B- K% A) P t6 @# B% X
% d% l( k# y& @/ x" f) J16行:将标志位置反,按键按一次置反一次(即0->1->0->1这样循环);
- f9 B! m3 H1 `. y# n( x: O
/ u9 |: F: G% J5 E) ^& X0 a17行:根据标志位“key_flag”的值,让LED灯亮或灭;
) B5 R2 a) _' t8 S8 B: w4 I8 w R) ~! Y' b
此时每按下一次按键键,蓝色LED灯将亮灭交替。
; a3 m2 f, x% p9 p* J4 ^6 U
- w4 M- A( K' v
$ M0 t1 m! M2 g ]4 u& o6 S$ j, O+ L# |8 T
4) 主函数测试% y1 ^2 H1 e6 C8 x) q
+ r" h" Q* u& Y: k" N! C( [0 @代码段 12.3.4 主函数控制逻辑(main.c); c3 P: n$ E+ w& ]
/ `7 T# |* @, z! t8 v
- // 初始化LED
4 n& [( J2 Z# }$ G - 0 S. j& L8 y* d( _" R2 N
- LedGpioInit();
. H7 Z" q; a' r - / Z6 n# b. M+ V" v8 a+ R4 {/ `
- // 初始化按键+ F, {, p* ?; g) g0 u
9 l1 h" w% e6 O# |7 } F& ]9 X2 b- KeyInit();3 J' l. w) h6 i3 j3 B% E& |9 h
- # F2 q1 H* }4 _. Y/ ]4 W- Z
: j8 Z6 s; w$ E! V6 R# E
1 y5 x: j. w7 l% U- while(1)
) h) S. }) M6 R; l" S: o
% l6 a9 D+ a9 G- {
3 k/ [2 y4 H. T& D1 U" ^7 l
! r$ c( t/ h2 d8 c8 O P- // 轮询按键键
! e, H& M2 C: r. z! R
- E: T: A3 f( j/ m# I- KeyPolling();
; r. X% v* C/ O, h; \ - 0 d% U* ]* Y& U+ P- `
- }
复制代码
# R- B6 U" V5 N* Z$ W/ S T6 z- p1~4行:初始化LED灯和按键;
& G- G; U1 r; @- v A; x0 t
' `7 K7 }* b& Q' L6~10行:一直循环查询每个按键当前状态,从而判断对应按键是否按下;
7 F+ B' Z* y1 H* O# a: c2 K& v' y+ Z. Y- U! _+ y% F
12.4 实验效果
; C- ~8 R1 E/ B本实验对应配套资料的“5_程序源码\5_GPIO—按键轮询\”。打开工程后,编译,下载。按下按键,用户LED灯亮/灭。 {9 t$ s# s! S" G+ B- O2 _3 B+ _% D
* y8 v" {( }4 I. y作者:攻城狮子黄
4 g8 O! N+ r( A, {5 d1 l1 y0 G+ N) R, k X1 w
1 c8 D. b$ B; ]% R" I0 |! q
|