CT107D上的键盘是4x4矩阵布局,总共16个按键(S4~S19),但实际代码里通常只用其中一部分。这一篇结合项目中的实际代码,把键盘扫描的原理、消抖策略和按键处理讲清楚。


硬件原理

CT107D的矩阵键盘布局:

         P3.3    P3.2    P3.1    P3.0
          |       |       |       |
         [S4]    [S8]    [S12]   [S16]   ← P4.4 控制(列1)
          |       |       |       |
         [S5]    [S9]    [S13]   [S17]   ← P4.2 控制(列2)
          |       |       |       |
         [S6]    [S10]   [S14]   [S18]   ← P3.5 控制(列3)
          |       |       |       |
         [S7]    [S11]   [S15]   [S19]   ← P3.4 控制(列4)

扫描原理就是逐列拉低,逐行检测

  1. 把某一列的IO拉低(比如P4.4=0),其余列保持高电平
  2. 检查P3.3/P3.2/P3.1/P3.0哪个被拉低了
  3. 如果P3.3=0,说明是S4被按下;P3.2=0说明是S5被按下,以此类推

有一个需要注意的地方:P3.4跟NE555频率发生器共用一个引脚。如果题目考到了NE555频率测量,就不能扫描第四列了。代码里的注释也说了:

// 若考到ne555 把P34全注释掉
P44 = 1; P42 = 1; P35 = 1; P34 = 1;

键盘扫描函数(项目实际代码)

以下是 Driver/Key.c 中的完整扫描代码(西风模板版本):

unsigned char Key_Read() {
   unsigned char Temp = 0;    // 一定要赋初值0!

    /* 第一列扫描(P4.4=0)*/
    P44 = 0; P42 = 1; P35 = 1; P34 = 1;
    if(P33 == 0) Temp = 4;     // S4
    if(P32 == 0) Temp = 5;     // S5
    if(P31 == 0) Temp = 6;     // S6
    if(P30 == 0) Temp = 7;     // S7

    /* 第二列扫描(P4.2=0)*/
    P44 = 1; P42 = 0; P35 = 1; P34 = 1;
    if(P33 == 0) Temp = 8;     // S8
    if(P32 == 0) Temp = 9;     // S9
    if(P31 == 0) Temp = 10;    // S10
    if(P30 == 0) Temp = 11;    // S11

    /* 第三列扫描(P3.5=0)*/
    P44 = 1; P42 = 1; P35 = 0; P34 = 1;
    if(P33 == 0) Temp = 12;    // S12
    if(P32 == 0) Temp = 13;    // S13
    if(P31 == 0) Temp = 14;    // S14
    if(P30 == 0) Temp = 15;    // S15

    /* 第四列扫描(P3.4=0)—— NE555时需要注释掉!*/
    P44 = 1; P42 = 1; P35 = 1; P34 = 0;
    if(P33 == 0) Temp = 16;    // S16
    if(P32 == 0) Temp = 17;    // S17
    if(P31 == 0) Temp = 18;    // S18
    if(P30 == 0) Temp = 19;    // S19

    return Temp;
}

两个关键细节:

  • Temp 必须初始化为0。如果没有按键按下,函数返回0。如果忘了初始化,返回值就是不确定的,按键处理会出错。
  • 每扫描完一列后恢复所有列为高电平P44=1; P42=1; P35=1; P34=1;),防止列之间互相干扰。

三态检测算法

西风模板里处理按键的核心逻辑:

idata uc Key_Val, Key_Old, Key_Down, Key_Up;

void Key_Proc() {
    Key_Val = Key_Read();

    Key_Down = Key_Val & (Key_Val ^ Key_Old);    // 按下检测
    Key_Up   = ~Key_Val & (Key_Val ^ Key_Old);   // 释放检测
    Key_Old  = Key_Val;                           // 更新旧值

    switch(Key_Down) {
        case 4:  // S4按下
            // ...
            break;
        case 8:  // S8按下
            // ...
            break;
    }
}

这个算法的好处在于:

  1. 一行代码检测按下和释放,不需要一堆if-else
  2. 天然消抖,配合10ms调度周期,按键抖动会被过滤
  3. 支持双按键,比如S8和S9同时按下

消抖的原理:机械按键按下时会有5~20ms的抖动期,电平会来回跳变。但因为Key_Proc在调度器里每10ms才执行一次,这10ms内不管抖动多少次都只读一次。

实际竞赛中常用的按键分配:

按键 常用功能
S4 (case 4) 模式切换/启动/确认
S5 (case 5) 复位/清除
S8 (case 8) 界面切换
S9 (case 9) 参数位置切换
S12 (case 12) 参数增加
S13 (case 13) 参数减少
S8+S9 (case 89) 双按键(特殊操作)

参数调节的边界保护

按键调参数时必须加边界保护,否则调过头会导致数据溢出或功能异常。第15届国赛中的例子:

case 12:  // 增加
    if(Seg_Disp_Mode == 2) {
        if(Param_Set_Pos == 0) {       // R参数(范围10~20)
            if(++Param_R_10x_Show > 20)
                Param_R_10x_Show = 20;  // 到上限就停
        } else {                        // B参数(范围-90~90)
            Param_B_Show += 5;
            if(Param_B_Show > 90)
                Param_B_Show = 90;
        }
    }
    break;

case 13:  // 减少
    if(Seg_Disp_Mode == 2) {
        if(Param_Set_Pos == 0) {
            if(--Param_R_10x_Show < 10)
                Param_R_10x_Show = 10;  // 到下限就停
        } else {
            Param_B_Show -= 5;
            if(Param_B_Show < -90)
                Param_B_Show = -90;
        }
    }
    break;

++ 在条件判断之前执行,所以用 > 20 而不是 > 19。这种"先变后判"的写法很常见。


第16届省赛的状态互锁设计

第16届省赛的按键处理有一个很好的设计——累加测量进行中时禁止某些操作

case 8:  // 界面切换
    if(measure_start_flag == 0 && ++seg_display_mode == 4)
        seg_display_mode = 1;
    break;
    // 累加测量中(measure_start_flag=1)不允许切换界面

case 12:  // 清除
    if(measure_start_flag == 0) {
        measure_mode = 0;
        seg_display_mode = 1;
        quench_flag = 1;
        distance = 0;
    }
    break;
    // 累加测量中不允许清除

这种"条件限制"的写法在竞赛中非常实用,可以防止用户在不合适的时候执行操作导致系统状态混乱。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐