第5章:硬件适配层应用——按键
本章导读
上一章 LED 是"输出"——芯片把电平送出去。这一章的按键是"输入"——外部把电平送进芯片。
输入比输出多两个麻烦:抖动(按下瞬间电平会跳几下)和识别(怎么知道按的是哪个键)。Z-Stack 的 HAL 已经帮我们封装好了按键驱动,这一章先看 GEC2530 底板的按键电路,再看 HAL 是怎么"读键"的。
一、先看电路:GEC2530 底板的按键(对照原理图)
① 独立按键:S3 / S4 / S5(Sheet 8)
打开原理图 Sheet 8,右下角是独立按键电路:
VDD_IO ──[R60 10K]──┬── S3 ── GND
└── [R64 0Ω]── P1_3
VDD_IO ──[R61 10K]──┬── S4 ── GND
└── [R66 0Ω]── P0_1(与 LED4 复用)
VDD_IO ──[R62 10K]──┬── S5 ── GND
└── [R68 0Ω]── P1_2(REMOTI_BUTTON)| 按键 | 引脚 | 电路特点 |
|---|---|---|
| S3 | P1_3 | 10K 上拉到 VDD_IO |
| S4 | P0_1(与 LED4 共用引脚) | 10K 上拉 |
| S5 | P1_2(REMOTI_BUTTON) | 10K 上拉 |
看懂这个电路:
- 上拉电阻(10K):按键没按时,引脚被上拉到高电平(读到 1);按下时,按键把引脚拉到地(读到 0)。所以按下 = 低电平;
- 引脚复用:S4 和 LED4 共用 P0_1——同一时刻要么当按键读、要么当灯控制,程序里要通过引脚功能配置切换。这种"一引脚多用途"在嵌入式里非常常见。
⚠️ 按键编号与宏的对应(容易踩坑):Z-Stack 3.0 默认的 hal_key.h 里,
HAL_KEY_SW_1~5是五向键、HAL_KEY_SW_6/HAL_KEY_SW_7才是独立按键(S1/S2)。默认配置中 S1 键 = P0_1(PUSH1_SBIT P0_1),对照本底板就是 S4。所以本章实验用 S4(P0_1)→HAL_KEY_SW_6,与原站一致。若想用 S3(P1_3)、S5(P1_2),先查工程 hal_key.c 的引脚映射,把代码里的宏换成实际对应的宏,不要猜。
② 五向摇杆按键:UP / DN / LT / RT / PUSH(Sheet 7)
打开原理图 Sheet 7,上半部分是五向按键处理电路:
UP(S6) ──[R38 100K 下拉]──┐
DN(S7) ──[R42 100K 下拉]──┤
LT(S8) ──[R40 100K 下拉]──┼── SN74HC32(或门 U9) ── KEY_MOVE ── P2_0
RT(S9) ──[R37 100K 下拉]──┤
PUSH(S10)─[R41 100K 下拉]─┘| 键 | 功能 | 引脚 |
|---|---|---|
| UP / DN / LT / RT / PUSH | 上 / 下 / 左 / 右 / 中间按下 | 5 键任意一个按下 → KEY_MOVE → P2_0 |
两个关键设计:
- 100K 下拉电阻:五向键没按时,或门输入为低;按下时(按键接 VDD)输入变高;
- SN74HC32 或门:5 个键只要任意一个按下,输出 KEY_MOVE 就变高,CC2530 只需一个引脚 P2_0 就能知道"摇杆被动了";至于具体是哪个方向,需要进一步用 ADC 读 KEY_LEVEL(Sheet 7 下半部分的 TLV272 运放把 5 个键分压成 5 个不同电压,P0_6 读电压即可区分)。
简单说:GEC2530 底板的按键 = 独立按键(P1_3/P0_1/P1_2,直接读电平)+ 五向摇杆(P2_0 检测按下、P0_6 读电压区分方向)。本章实验用独立按键就够入门了。
二、先把"回调函数"讲透
① 什么是回调函数?
回调就是:你把一个函数"登记"给 HAL,HAL 在按键事件发生时,反过来调用你登记的这个函数。
生活类比:你给前台(HAL)留了一个电话号码(回调函数名),并交代"有按键这种事就打这个电话"。前台平时不打扰你,一旦按键发生,就主动拨号(调用你的函数)通知你。
关键点:不是你去调 HAL,而是 HAL 来调你——所以叫"回(反)调(用)"。
② 回调函数长什么样?—— halKeyCBack_t
HAL 头文件里定义了一个"回调函数类型":
typedef void (*halKeyCBack_t)(uint8 keys, uint8 shift);
// ↑ 回调函数指针 ↑ 哪个键 ↑ 修饰键| 参数 | 含义 | 大白话 |
|---|---|---|
keys | 按键状态,按位表示 | 哪个键被按了(可以多个同时按) |
shift | 修饰键(本实验用不到) | 相当于键盘的 Ctrl/Shift |
SampleSwitch 里真实的应用回调长这样(注意是两个参数,顺序 shift 在前、keys 在后):
static void zclSampleSw_HandleKeys(byte shift, byte keys) {
// 你的逻辑写在这里
}判断哪个键? 用 keys & HAL_KEY_SW_x 按位判断。按键编号宏(hal_key.h 默认):
#define HAL_KEY_SW_1 0x01 // Joystick up(五向键·上)
#define HAL_KEY_SW_2 0x02 // Joystick right(五向键·右)
#define HAL_KEY_SW_5 0x04 // Joystick center(五向键·中)
#define HAL_KEY_SW_4 0x08 // Joystick left(五向键·左)
#define HAL_KEY_SW_3 0x10 // Joystick down(五向键·下)
#define HAL_KEY_SW_6 0x20 // Button S1(独立按键,默认接 P0_1)
#define HAL_KEY_SW_7 0x40 // Button S2(独立按键)⚠️ 别把
HAL_KEY_SW_1当独立按键——它默认是五向键"上"!独立按键要用HAL_KEY_SW_6/HAL_KEY_SW_7(本底板 S4=P0_1 对应 SW_6)。
③ 回调怎么"登记"?—— HalKeyConfig()
void HalKeyConfig(bool interruptEnable, halKeyCBack_t cback);| 参数 | 含义 |
|---|---|
interruptEnable | TRUE=用中断方式;FALSE=用轮询方式 |
cback | 你要登记的回调函数名 |
这个函数内部只做了一件关键的事:把你的回调函数名存到一个全局变量里:
pHalKeyProcessFunction = cback; // 把回调函数指针存起来,随时候命之后 HAL 一检测到按键变化,就通过这个存好的指针调用你的函数。没登记(没调 HalKeyConfig),回调就不会生效。
④ 回调什么时候被调用?—— 只在"状态变化"的瞬间
这是最容易误解的点:回调不是在"按住期间"一直被调用,而是在"按下"和"松开"这两个变化瞬间各调一次。
void HalKeyPoll(void) {
keys = 读取按键电平...;
if (keys != halKeySavedKeys) { // ★ 只有状态变了才进入
valid = TRUE;
halKeySavedKeys = keys; // 记住新状态
}
if (valid && pHalKeyProcessFunction) {
pHalKeyProcessFunction(keys, shift); // 调回调
}
}- 按下 S3:状态从"没按"变"按下" → 调一次回调(
keys & HAL_KEY_SW_1为真); - 松开 S3:状态从"按下"变"松开" → 再调一次回调(
keys & HAL_KEY_SW_1为假)。
这个"变了才报"的设计同时起到了防抖作用:抖动产生的电平跳变如果没造成状态变化,就不会重复触发。所以回调里用
if/else分别处理"按下"和"松开"两个时刻。
三、完整链路:一次按键的"旅行"
把上面串起来,一次按键从物理到你的代码,一共经过 7 站:
① 你按下 S4(P0_1,原站 S1 键)
↓
② P0_1 电平由高变低(上拉电阻,按下为低)
↓
③ HAL 检测:轮询(100ms 查一次)或中断(电平一变就触发)
↓
④ HalKeyPoll() 扫描按键,发现状态变化(防抖)
↓
⑤ 调系统回调 OnBoard_KeyCallback(keys, shift)——协议栈已写好
↓
⑥ OnBoard 把按键打包成 KEY_CHANGE 消息,osal_msg_send() 发给应用层任务
↓
⑦ 应用层收到 SYS_EVENT_MSG,拆出消息,调用你写的 zclSampleSw_HandleKeys()
↓
⑧ 你在回调里判断按键、操作 LED —— ★ 你要写的就这一站各角色分工:
| 角色 | 谁 | 干什么 | 要不要你写 |
|---|---|---|---|
| 硬件 | S3(P1_3) | 产生电平变化 | 不用 |
| HAL 检测 | HalKeyPoll() | 扫描 + 防抖 | 不用 |
| 系统回调 | OnBoard_KeyCallback() | 打包成 KEY_CHANGE 消息 | 不用 |
| 消息路由 | osal_msg_send/receive | 把消息送到应用层 | 不用 |
| 应用回调 | zclSampleSw_HandleKeys() | 判断按键、干正事 | ★ 要你写 |
也就是说:框架把"按键 → 电平 → 扫描 → 打包消息 → 送到你家门口"全包了,你只需要写"收到信后怎么办"(回调函数的内容)。这正是协议栈"分层 + 回调"设计的精髓。
四、按键实验:按下 S4 点亮 LED1
实验目标
在 SampleSwitch 工程上:按下 S4(P0_1)→ LED1(P1_0)点亮;松开 → LED1 熄灭。
第 1 步:确认按键引脚配置
在 HAL 板级配置文件(hal_board_cfg.h)中确认独立按键 S1 的引脚配置:
/* S1 按键(对照本底板 = S4,P0_1) */
#define PUSH1_BV BV(1)
#define PUSH1_SBIT P0_1
#define PUSH1_POLARITY ACTIVE_LOW // 按下为低电平对照原理图:S4 = P0_1,10K 上拉,按下为低电平。
hal_key.c内部会做电平极性处理(PUSH1_POLARITY ACTIVE_LOW),应用层只需关心"有没有按下"。
第 2 步:应用层注册"我要收按键消息"
在 zclSampleSw_Init() 里加一行,告诉系统"按键消息发给我":
void zclSampleSw_Init(byte task_id) {
// ……原有初始化代码……
RegisterForKeys(zclSampleSw_TaskID); // 注册接收按键消息
}第 3 步:写应用回调函数(★核心)
static void zclSampleSw_HandleKeys(byte shift, byte keys) {
if (keys & HAL_KEY_SW_6) { // 按下 S4(P0_1,默认 S1 键)
HalLedSet(HAL_LED_1, HAL_LED_MODE_ON); // → 点亮 LED1
} else { // 松开 S4(变化瞬间 keys 该位=0)
HalLedSet(HAL_LED_1, HAL_LED_MODE_OFF); // → 熄灭 LED1
}
}逐行解释:
| 代码 | 含义 |
|---|---|
keys & HAL_KEY_SW_6 | 按位与:看 keys 里独立按键 S1 对应位是不是 1(HAL_KEY_SW_6=0x20) |
HalLedSet(..., ON) | 按下瞬间执行:LED1 亮 |
else 分支 | 松开瞬间也会调一次回调(状态变化),此时 keys 的 S1 位=0 → 熄灭 |
⚠️ 若你的工程把独立按键映射到别的引脚(比如你非要用 S3/P1_3),先查 hal_key.c 的映射,把
HAL_KEY_SW_6换成实际对应的宏。
第 4 步:事件处理函数收消息(框架已写好,看懂即可)
SampleSwitch 的事件处理函数里已有这段(不用你写,但要看懂):
uint16 zclSampleSw_event_loop(uint8 task_id, uint16 events) {
if (events & SYS_EVENT_MSG) {
uint8 *msg_ptr;
while ((msg_ptr = osal_msg_receive(zclSampleSw_TaskID)) != NULL) {
if (*msg_ptr == KEY_CHANGE) { // 是按键变化消息
// 消息第 2 个字节带着 keys,交给你的回调
zclSampleSw_HandleKeys(0, *(uint8 *)(msg_ptr + 1));
}
osal_msg_deallocate(msg_ptr); // 用完释放(内存铁律)
}
return (events ^ SYS_EVENT_MSG);
}
return 0;
}第 5 步:编译下载验证
- Rebuild All 编译 → 烧录到 GEC2530 开发板;
- 按下 S4:LED1 点亮;
- 松开 S4:LED1 熄灭。
实验变式:
- 把
ON/OFF换成HAL_LED_MODE_TOGGLE,就是"按一下亮、再按一下灭"(翻转);- 把回调里换成
HalLedBlink(HAL_LED_1, 5, 50, 500),按键触发"闪 5 下";- 用五向摇杆:按下任意方向,P2_0(KEY_MOVE)触发后,再通过 ADC 读 P0_6 电压区分方向,控制不同 LED 亮——这就是完整的方向识别实验(涉及 ADC,见第 8 章)。
常见坑提醒:
- 回调签名必须是两个参数
(byte shift, byte keys),顺序别搞反,否则编译报错;- 回调是变化触发,按住期间不会反复调用——想"一直亮"就在按下时 ON、松开时 OFF(用 if/else);
- 忘了
RegisterForKeys(),消息不会送上门,按键"没反应"先查这一行。
五、小结
| 问题 | 答案 |
|---|---|
| GEC2530 独立按键有哪些? | S3→P1_3、S4→P0_1(与 LED4 复用)、S5→P1_2 |
| 本实验用哪个键? | S4(P0_1)= Z-Stack 默认 S1 键 = HAL_KEY_SW_6(与原站一致) |
| 按键电路什么特点? | 10K 上拉,按下为低电平(PUSH1_POLARITY ACTIVE_LOW) |
| 五向摇杆怎么接的? | 5 键经 SN74HC32 或门 → KEY_MOVE→P2_0;方向由 P0_6 读电压区分 |
| 按键编号宏怎么对应? | 默认 SW_1~5=五向键、SW_6/SW_7=独立键;以工程 hal_key.c 映射为准 |
| 什么是回调函数? | 登记给 HAL、由 HAL 在按键事件发生时反过来调用你的函数 |
| 回调的签名? | (byte shift, byte keys) 两个参数,keys & HAL_KEY_SW_x 判断哪个键 |
| 回调什么时候被调? | 状态变化瞬间(按下调一次、松开调一次),不是按住期间一直调 |
| 回调怎么生效? | HalKeyConfig() 存函数指针 + 应用层 RegisterForKeys() 登记接收消息 |
| 一次按键走几站? | 硬件电平 → HalKeyPoll 扫描防抖 → OnBoard 打包消息 → 消息路由 → 你的回调(只有最后一站要你写) |
| 按键是"输入"还是"输出"? | 输入:外部电平送进芯片,靠上拉电阻保证未按下时电平确定 |
一句话总结:按键实验 = "留电话(登记回调)→ 系统送信(电平→扫描→消息)→ 你接电话(回调里判断按键干正事)"。回调是"系统反过来调你"的机制,只写最后一步,你就能让开发板"听懂"人手——按键 + LED 组合起来,你已经能在协议栈里做"人机交互"了。
课堂练习:按键
依据第5章:按键电路、回调机制、RegisterForKeys、宏编号作答。点击选项即可作答、即时反馈对错。
按键电路、回调机制、RegisterForKeys、宏编号
1 GEC2530 底板独立按键 S4 接在哪个引脚?
2 按键电路有什么特点?
3 按键回调函数什么时候被调用?
4 为什么必须调用 RegisterForKeys()?
5 关于按键宏编号,下列说法正确的是?
内容来源:紫甲 IoT《ZigBee 3.0 开发指南》第三部分·第5章(5.1~5.2),经本站整理优化以便于理解,电路部分已对照 GEC2530 开发板底板原理图核对;原站链接