Skip to content

第5章:硬件适配层应用——按键 ​

本章导读

上一章 LED 是"输出"——芯片把电平送出去。这一章的按键是"输入"——外部把电平送进芯片。

输入比输出多两个麻烦:抖动(按下瞬间电平会跳几下)和识别(怎么知道按的是哪个键)。Z-Stack 的 HAL 已经帮我们封装好了按键驱动,这一章先看 GEC2530 底板的按键电路,再看 HAL 是怎么"读键"的。

一、先看电路:GEC2530 底板的按键(对照原理图) ​

① 独立按键:S3 / S4 / S5(Sheet 8) ​

打开原理图 Sheet 8,右下角是独立按键电路:

text
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)
按键引脚电路特点
S3P1_310K 上拉到 VDD_IO
S4P0_1(与 LED4 共用引脚)10K 上拉
S5P1_2(REMOTI_BUTTON)10K 上拉

看懂这个电路:

  1. 上拉电阻(10K):按键没按时,引脚被上拉到高电平(读到 1);按下时,按键把引脚拉到地(读到 0)。所以按下 = 低电平;
  2. 引脚复用: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,上半部分是五向按键处理电路:

text
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

两个关键设计:

  1. 100K 下拉电阻:五向键没按时,或门输入为低;按下时(按键接 VDD)输入变高;
  2. 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 头文件里定义了一个"回调函数类型":

c
typedef void (*halKeyCBack_t)(uint8 keys, uint8 shift);
//                ↑ 回调函数指针          ↑ 哪个键   ↑ 修饰键
参数含义大白话
keys按键状态,按位表示哪个键被按了(可以多个同时按)
shift修饰键(本实验用不到)相当于键盘的 Ctrl/Shift

SampleSwitch 里真实的应用回调长这样(注意是两个参数,顺序 shift 在前、keys 在后):

c
static void zclSampleSw_HandleKeys(byte shift, byte keys) {
    // 你的逻辑写在这里
}

判断哪个键? 用 keys & HAL_KEY_SW_x 按位判断。按键编号宏(hal_key.h 默认):

c
#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() ​

c
void HalKeyConfig(bool interruptEnable, halKeyCBack_t cback);
参数含义
interruptEnableTRUE=用中断方式;FALSE=用轮询方式
cback你要登记的回调函数名

这个函数内部只做了一件关键的事:把你的回调函数名存到一个全局变量里:

c
pHalKeyProcessFunction = cback;   // 把回调函数指针存起来,随时候命

之后 HAL 一检测到按键变化,就通过这个存好的指针调用你的函数。没登记(没调 HalKeyConfig),回调就不会生效。

④ 回调什么时候被调用?—— 只在"状态变化"的瞬间 ​

这是最容易误解的点:回调不是在"按住期间"一直被调用,而是在"按下"和"松开"这两个变化瞬间各调一次。

c
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 站:

text
① 你按下 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 的引脚配置:

c
/* 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() 里加一行,告诉系统"按键消息发给我":

c
void zclSampleSw_Init(byte task_id) {
    // ……原有初始化代码……

    RegisterForKeys(zclSampleSw_TaskID);   // 注册接收按键消息
}

第 3 步:写应用回调函数(★核心) ​

c
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 的事件处理函数里已有这段(不用你写,但要看懂):

c
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 步:编译下载验证 ​

  1. Rebuild All 编译 → 烧录到 GEC2530 开发板;
  2. 按下 S4:LED1 点亮;
  3. 松开 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 关于按键宏编号,下列说法正确的是?

已完成 0 / 5 题

内容来源:紫甲 IoT《ZigBee 3.0 开发指南》第三部分·第5章(5.1~5.2),经本站整理优化以便于理解,电路部分已对照 GEC2530 开发板底板原理图核对;原站链接

aioteer · 物联网专业课程群

💬AI 伴学配置