Skip to content

5.1 按键实验 ​

本节导读

LED 会亮了,但只会"自己动"太没意思——让按键来指挥 LED!

本实验目标(与原站一致):按下按键 → LED 闪烁。学会按键的使用套路后,你就能让开发板"听懂"人手了。

一、先搞清一件事:按键编号到底是多少? ​

很多同学在这里栽跟头——宏名 HAL_KEY_SW_x 和开发板上的按键不是"想当然"对应,它由工程里的 hal_key.c 映射决定。

Z-Stack 3.0 默认的按键宏(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(独立按键 S1)
#define HAL_KEY_SW_7  0x40   // Button S2(独立按键 S2)

重点:SW_1~SW_5 是五向键(用得少),SW_6 和 SW_7 才是独立按键!所以别拿 HAL_KEY_SW_1 当独立按键用——它默认是五向键"上"。

引脚对接配置(hal_board_cfg.h) ​

HAL 用 PUSH1/PUSH2 把"独立按键"接到具体引脚:

c
/* S1 按键 = P0_1(原站配套板与 GEC2530 底板一致) */
#define PUSH1_BV    BV(1)
#define PUSH1_SBIT  P0_1

// 按键按下时的电平极性(按下为低电平 → ACTIVE_LOW)
#define PUSH1_POLARITY ACTIVE_LOW

对照 GEC2530 底板原理图(Sheet 8):P0_1 上接的独立按键是 S4。所以在本站:S4(P0_1)= Z-Stack 默认的 S1 键 = HAL_KEY_SW_6——本章实验就用它,与原站一致。

⚠️ 想用板上其他独立按键(如 S3/P1_3、S5/P1_2)?先查你工程的 hal_key.c,看它把哪个引脚映射到哪个 HAL_KEY_SW_x,再把代码里的宏换成实际的宏。不要猜。

二、三步用法:登记 → 写回调 → 处理 ​

第 1 步:应用层登记"我要收按键消息" ​

按键受 OSAL 调度,用之前必须在应用层注册。在 zclSampleSw_Init() 里(SampleSwitch 默认已有一行):

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

    RegisterForKeys(zclSampleSw_TaskID);   // 注册:按键消息发给我
}

为什么必须 RegisterForKeys()? ​

一句话:按键消息需要一个"收件地址",RegisterForKeys() 就是提前把地址登记给系统。不登记,按键事件就"无处可投"。

① 按键消息不是直接发给你的

text
按键按下 → HAL 检测(HalKeyPoll)→ 系统回调 OnBoard_KeyCallback(打包消息)
→ osal_msg_send(发给谁?) → 应用层收到 KEY_CHANGE

HAL 层不认识应用层(分层设计),它把按键事件交给中间人 OnBoard;而 OnBoard 打包好消息后,必须知道投递地址才能发出去。

② RegisterForKeys() 干的就是"登记地址"(OnBoard.c 源码)

c
static uint8 RegisteredTaskID = NO_TASK;   // ★ 全局只有一个"收件人"变量

uint8 RegisterForKeys(uint8 task_id) {
    if (RegisteredTaskID == NO_TASK) {     // 只允许登记一次
        RegisteredTaskID = task_id;        // 把你的任务 ID 记为收件人
        return TRUE;
    }
    return FALSE;
}

按键发生时:

c
void OnBoard_KeyCallback(uint8 keys, uint8 shift) {
    // ……打包 KEY_CHANGE 消息……
    if (RegisteredTaskID != NO_TASK) {          // 有登记过收件人吗?
        osal_msg_send(RegisteredTaskID, msg);   // 有 → 投递
    }
    // 没有登记 → 消息直接丢弃!
}

③ 不用的后果:RegisteredTaskID 保持 NO_TASK,消息被直接丢弃 → 应用层永远收不到 KEY_CHANGE → 按键"按了没反应"。

④ 为什么定时器事件不用登记,按键就要?

事件类型谁产生地址从哪来
定时器事件你自己调 osal_start_timerEx(task_id, ...)调用时你亲手填了地址,不用登记
按键事件HAL 层检测(按键瞬间你不在场)必须提前登记,让系统记住你的地址

⑤ 为什么只允许一个任务登记? 协议栈里 ZDO、ZCL、应用层都是任务,都收会重复处理。所以设计成单接收者——SampleSwitch 里应用层 zclSampleSw_Init() 最后初始化,抢先登记成功。

生活类比:物业(OnBoard)收到按键快递(KEY_CHANGE 消息),但只有一个收件箱(RegisteredTaskID)。你提前登记"我的信箱号"(RegisterForKeys),快递才会投给你;不登记,快递员不知道给谁,快递就被退回去了。

第 2 步:写应用回调函数(★你只写这个) ​

c
static void zclSampleSw_HandleKeys(byte shift, byte keys) {
    if (keys & HAL_KEY_SW_6) {                     // 按下了独立按键 S1(S4/P0_1)?
        // 点亮并闪烁 LED1(原站实验:按键 → 闪烁 LED)
        HalLedSet(HAL_LED_1, HAL_LED_MODE_ON);
        HalLedBlink(HAL_LED_1, 10, 50, 400);       // 闪 10 次:亮 200ms 灭 200ms
    }
}

回调签名必须是两个参数 (byte shift, byte keys)——shift 是修饰键(一般不用),keys 是按位打包的按键状态,用 keys & HAL_KEY_SW_x 判断哪个键。

生活类比:回调就是你留给前台的电话号码——按键事件发生,HAL 反过来打电话(调用)给你。你只写"接到电话后怎么办"。

第 3 步:事件处理函数收消息(框架已写好,看懂即可) ​

注册后按键被按下,会产生应用层系统事件 KEY_CHANGE。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;
}

不同版本写法略有差异(有的用 switch (MSGpkt->hdr.event) { case KEY_CHANGE: ... }),本质相同:消息头是 KEY_CHANGE → 取出按键状态 → 调回调。

三、启用按键的宏定义(别漏!) ​

按键驱动默认已启用(HAL_KEY=TRUE 在 hal_board_cfg.h 中)。若你的工程没启用,在 IAR 的 Options → C/C++ Compiler → Preprocessor → Defined symbols 中添加:

text
HAL_KEY=TRUE

可选:想用中断方式检测按键(按下立刻响应、更省电),再加一行:

text
ISR_KEYINTERRUPT

不加 ISR_KEYINTERRUPT 就是轮询方式(HAL 每 100ms 查一次)。两种方式的区别和原理,见 5.2 选修。

四、完整实验流程 ​

  1. 打开工程:SampleSwitch;
  2. 确认登记:zclSampleSw_Init() 里有 RegisterForKeys(zclSampleSw_TaskID);;
  3. 写回调:实现 zclSampleSw_HandleKeys(见上文第 2 步,判断 HAL_KEY_SW_6);
  4. 确认宏:HAL_KEY=TRUE 已定义(必要时加 ISR_KEYINTERRUPT);
  5. 编译:右键工程 → Rebuild All,确认 0 Error(s);
  6. 下载运行:Download and Debug → Go;
  7. 测试:按下 S4(P0_1),LED1 点亮并闪烁 10 次。

编译成功提示

五、变式挑战(课后) ​

挑战提示
按一下亮、再按一下灭回调里用 HalLedSet(HAL_LED_1, HAL_LED_MODE_TOGGLE)
按键让红/绿/蓝三色 LED 分别闪烁(原站版)if(keys & HAL_KEY_SW_6) 里分别调 HalLedBlink(HAL_LED_1, 10, 50, 400) / HAL_LED_2, 1000 / HAL_LED_3, 2000
用板上其他按键(S3/S5)先查工程 hal_key.c 的引脚映射,换对应 HAL_KEY_SW_x
长按 1 秒才响应配合定时器事件计时(3.3 套路)
想搞懂按键"为什么这么工作"见 5.2 选修:HAL 按键框架详解

小结 ​

步骤做什么代码
登记告诉系统"按键消息发给我"RegisterForKeys(zclSampleSw_TaskID)
写回调判断按了哪个键、干什么if (keys & HAL_KEY_SW_6) HalLedBlink(...)
开宏启用按键驱动(可选中断)HAL_KEY=TRUE(ISR_KEYINTERRUPT)

一句话总结:按键实验 = "登记(RegisterForKeys)→ 写回调(判断 HAL_KEY_SW_x)→ 开宏(HAL_KEY=TRUE)",框架负责把"按键变化"打包成 KEY_CHANGE 消息送上门,你只写"收到后怎么办"。注意按键编号以工程 hal_key.c 映射为准,别把五向键宏当独立键用!


内容来源:紫甲 IoT《ZigBee 3.0 开发指南》第三部分·5.1 节(原站:按下按键闪烁 LED),本站按 GEC2530 开发板(对照底板原理图)与 SampleSwitch 工程重写;原站链接

aioteer · 物联网专业课程群

💬AI 伴学配置