Manual Scan Mode

该示例使用 Keyscan 的手动扫描模式,实现矩阵键盘的扫描功能。

示例中使用软件定时器实现按键按下或释放的去抖功能。

当单次扫描结束时,进入 Keyscan 单次扫描完成中断,在中断处理函数内读取按键信息。

环境需求

该示例支持以下开发套件:

开发套件

Hardware Platforms

Board Name

RTL8752H HDK

RTL8752H EVB

更多信息请参考 快速入门

硬件连线

EVB 外接矩阵键盘,连接 P2_3 和 ROW0,P2_4 和 ROW1,P4_0 和 COLUMN0,P4_1 和 COLUMN1。

外接矩阵键盘的原理图如下图所示:

这里应该是外接矩阵键盘的图片

外接矩阵键盘

编译和下载

该示例的工程路径如下:

Project file: board\evb\io_sample\KEYSCAN\Keyscan_manual\mdk

Project file: board\evb\io_sample\KEYSCAN\Keyscan_manual\gcc

请按照以下步骤操作构建并运行该示例:

  1. 打开工程文件。

  2. 按照 快速入门编译 APP Image 给出的步骤构建目标文件。

  3. 编译成功后,在路径 mdk\bingcc\bin 下会生成 app bin app_MP_xxx.bin 文件。

  4. 按照 快速入门MP Tool 给出的步骤将 app bin 烧录至 EVB 内。

  5. 按下 reset 按键,开始运行。

测试验证

  1. 初始化软件定时器,打印 log。

    [io_keyscan] timer_keyscan_init: keyscan timer init
    
  2. 当检测到按键按下后,打印按键信息。

    1. 若检测到单个按键按下,打印如下 log。

      [io_keyscan] io_keyscan_handle_keys: Single key press. key: (x, x)
      
    2. 若检测到两个按键按下,打印如下 log。

      [io_keyscan] io_keyscan_handle_keys: Two key press. key0: (x, x), key1: (x, x)
      
  3. 当按键全部松开后,打印如下 log。

    [io_keyscan] io_keyscan_handle_keys: All keys release.
    

代码介绍

该章节分为以下几个部分:

  1. 源码路径

  2. 初始化函数将在 初始化 章节介绍。

  3. 初始化后的功能实现将在 功能实现 章节介绍。

源码路径

  • 工程路径: sdk\board\evb\io_sample\KEYSCAN\Keyscan_manual

  • 源码路径: sdk\src\sample\io_sample\KEYSCAN\Keyscan_manual

该工程的工程文件代码结构如下:

└── Project: keyscan_manual
    └── secure_only_app
        └── include
            ├── app_define.h
            └── rom_uuid.h
        ├── cmsis                    includes CMSIS header files and startup files
            ├── overlay_mgr.c
            ├── system_rtl876x.c
            └── startup_rtl876x.s
        ├── lib                      includes all binary symbol files that user application is built on
            ├── rtl8752h_sdk.lib
            ├── gap_utils.lib
            └── ROM.lib
        ├── peripheral               includes all peripheral drivers and module code used by the application
            ├── rtl876x_rcc.c
            ├── rtl876x_pinmux.c
            ├── rtl876x_nvic.c
            ├── rtl876x_gdma.c
            └── rtl876x_keyscan.c
        ├── profile
        └── app                      includes the ble_peripheral user application implementation
            ├── main.c
            ├── ancs.c
            ├── app.c
            ├── app_task.c
            ├── app_timer.c
            └── io_keyscan.c

初始化

当 EVB 复位启动时,调用 main() 函数,将执行以下流程:

int main(void)
{
    extern uint32_t random_seed_value;
    srand(random_seed_value);
    global_data_init();
    board_init();
    le_gap_init(APP_MAX_LINKS);
    gap_lib_init();
    app_le_gap_init();
    app_le_profile_init();
    pwr_mgr_init();
    sw_timer_init();
    task_init();
    os_sched_start();

    return 0;
}

备注

le_gap_init()gap_lib_init()app_le_gap_initapp_le_profile_init 等为 privacy 管理模块相关的初始化,参考 LE Peripheral Privacy 中的初始化流程介绍。

与外设相关的初始化流程具体如下:

  1. global_data_init 中,执行 global_data_keyscan_init ,初始化全局变量 Current_Key_Data

    void global_data_keyscan_init(void)
    {
        /* Data struct init */
        memset(&Current_Key_Data, 0, sizeof(KeyScan_Data_TypeDef));
    }
    
  2. board_init 中,执行 board_keyboard_init ,该函数为 PAD/PINMUX 设置,包含如下流程:

    1. 配置 PAD:设置引脚、PINMUX 模式、PowerOn。其中 ROW 相关引脚设置内部上拉,输出失能,COL 相关引脚无内部上拉,输出低。

    2. 配置 PINMUX:分配引脚分别为 KEY_ROW_0、KEY_ROW_1、KEY_COL_0、KEY_COL_1 功能。

    void board_keyboard_init(void)
    {
        /* Keypad pad config */
        Pad_Config(KEYBOARD_ROW_0, PAD_PINMUX_MODE, PAD_IS_PWRON, PAD_PULL_UP, PAD_OUT_DISABLE,
                PAD_OUT_LOW);
        Pad_Config(KEYBOARD_ROW_1, PAD_PINMUX_MODE, PAD_IS_PWRON, PAD_PULL_UP, PAD_OUT_DISABLE,
                PAD_OUT_LOW);
        Pad_Config(KEYBOARD_COLUMN_0, PAD_PINMUX_MODE, PAD_IS_PWRON, PAD_PULL_NONE, PAD_OUT_ENABLE,
                PAD_OUT_LOW);
        Pad_Config(KEYBOARD_COLUMN_1, PAD_PINMUX_MODE, PAD_IS_PWRON, PAD_PULL_NONE, PAD_OUT_ENABLE,
                PAD_OUT_LOW);
    
        /* keypad pinmux config */
        Pinmux_Config(KEYBOARD_ROW_0, KEY_ROW_0);
        Pinmux_Config(KEYBOARD_ROW_1, KEY_ROW_1);
        Pinmux_Config(KEYBOARD_COLUMN_0, KEY_COL_0);
        Pinmux_Config(KEYBOARD_COLUMN_1, KEY_COL_1);
    }
    
  3. 在执行 os_sched_start() 开启任务调度后,在 app_main_task 主任务内,执行 driver_init 对外设驱动进行初始化配置。

  4. driver_init 中执行 driver_keyboard_init ,该函数为 Keyscan 外设的初始化,包含如下流程:

    1. 使能 RCC 时钟。

    2. 配置 Keyscan 的 rowSize 和 colSize 为(2*2)。

    3. 配置扫描模式为 Manual Mode。

    4. 使能 Keyscan 的硬件去抖功能。

    5. 配置 Keyscan 单次扫描结束中断 KEYSCAN_INT_SCAN_END

    6. 使能 Keyscan 外设。

    void driver_keyboard_init(uint32_t vDebounce_En)
    {
        /* Turn on keyscan clock */
        RCC_PeriphClockCmd(APBPeriph_KEYSCAN, APBPeriph_KEYSCAN_CLOCK, ENABLE);
    
        KEYSCAN_InitTypeDef KEYSCAN_InitStruct;
        KeyScan_StructInit(&KEYSCAN_InitStruct);
    
        KEYSCAN_InitStruct.rowSize  = KEYBOARD_ROW_SIZE;
        KEYSCAN_InitStruct.colSize  = KEYBOARD_COLUMN_SIZE;
        KEYSCAN_InitStruct.scanmode     = KeyScan_Manual_Scan_Mode;
        KEYSCAN_InitStruct.debounceEn   = vDebounce_En;
    
        KeyScan_Init(KEYSCAN, &KEYSCAN_InitStruct);
    
        KeyScan_INTConfig(KEYSCAN, KEYSCAN_INT_SCAN_END, ENABLE);
        KeyScan_ClearINTPendingBit(KEYSCAN, KEYSCAN_INT_SCAN_END);
        KeyScan_INTMask(KEYSCAN, KEYSCAN_INT_SCAN_END, DISABLE);  /* Unmask keyscan interrupt */
        KeyScan_Cmd(KEYSCAN, ENABLE);
    
        /* Keyscan IRQ */
        NVIC_InitTypeDef NVIC_InitStruct;
        NVIC_InitStruct.NVIC_IRQChannel         = KeyScan_IRQn;
        NVIC_InitStruct.NVIC_IRQChannelCmd      = (FunctionalState)ENABLE;
        NVIC_InitStruct.NVIC_IRQChannelPriority = 3;
        NVIC_Init(&NVIC_InitStruct);
    }
    
  5. sw_timer_init 中执行 timer_keyscan_init ,该函数为软件定时器的初始化,包含如下流程:

    1. 执行 os_timer_create() 函数,创建软件定时器,定时时间为 200ms,定时结束后执行 timer_keyscan_callback 回调函数,用于模拟 Keyscan 按键按下的 Debounce。

    void timer_keyscan_init(void)
    {
        APP_PRINT_INFO0("[io_keyscan] timer_keyscan_init: keyscan timer init");
        if (false == os_timer_create(&KeyScan_Timer_Handle, "keyscan_timer",  1, \
                                    KEYSCAN_SW_INTERVAL, false, timer_keyscan_callback))
        {
            APP_PRINT_ERROR0("[io_keyscan] timer_keyscan_init: timer creat failed!");
        }
    }
    

功能实现

程序开始会启动一个 200ms 的软件定时器并使能 Keyscan 外设进行一次手动扫描。扫描结束后会进入单次扫描结束中断。 如果检测到有按键按下,则将 Key_Pressed_Flag 置 1,重启 200ms 定时器,重新使能 Keyscan 开始下一次扫描。

重复上述过程,当 200ms 定时器到期后,重新启动 10ms 的软件定时器用于模拟按键松开去抖。 10ms 定时器到期后判断 Key_Pressed_Flag ,如果为 0 代表扫描到所有按键松开,反之则仍有按键按下。

  1. 初始化完毕之后,执行 os_sched_start() 函数,开始任务调度。

  2. 当有按键按下时,开启手动扫描模式。当单次扫描完成后,进入 Keyscan 中断函数。

    1. 屏蔽 Keyscan 单次扫描结束中断。

    2. 读取 Keyscan FIFO 中的数据长度和数据内容。

    3. Key_Pressed_Flag 置为 true ,表明当前有按键按下。

    4. 执行 os_timer_restart() 重启软件定时器,定时时间为 200ms,继续检测按键按下信息,模拟按下 Debounce 功能。

    5. 定义消息类型 IO_MSG_TYPE_KEYSCAN,定义消息子类型为 IO_MSG_KEYSCAN_RX_PKT ,发送 msg 给 task。主 task 检测到消息类型时,执行 io_keyscan_handle_keys 打印对应的按键信息。

    void Keyscan_Handler(void)
    {
        uint32_t fifo_length;
        T_IO_MSG int_keyscan_msg;
    
        if (KeyScan_GetFlagState(KEYSCAN, KEYSCAN_INT_FLAG_SCAN_END) == SET)
        {
            /* Read current keyscan interrupt status and mask interrupt */
            KeyScan_INTMask(KEYSCAN, KEYSCAN_INT_SCAN_END, ENABLE);
            memset(&Current_Key_Data, 0, sizeof(KeyScan_Data_TypeDef));
    
            /* KeyScan fifo not empty */
            if (KeyScan_GetFlagState(KEYSCAN, KEYSCAN_FLAG_EMPTY) != SET)
            {
                fifo_length = (uint32_t)KeyScan_GetFifoDataNum(KEYSCAN);
                KeyScan_Read(KEYSCAN, (uint16_t *)&Current_Key_Data.key[0], fifo_length);
                Current_Key_Data.length = fifo_length;
                Key_Pressed_Flag = true;
    
                /* Start sw timer to check press status */
                if (!os_timer_restart(&KeyScan_Timer_Handle, KEYSCAN_SW_INTERVAL))
                {
                    APP_PRINT_ERROR0("[io_keyscan] Keyscan_Handler: Restart keyscan_timer failed!");
                    /* Set flag to default status and reinit keyscan module with debounce enabled */
                    global_data_keyscan_init();
                    driver_keyboard_init(KeyScan_Debounce_Enable);
                    return;
                }
    
                /* Send event to app task */
                int_keyscan_msg.type = IO_MSG_TYPE_KEYSCAN;
                int_keyscan_msg.subtype = IO_MSG_KEYSCAN_RX_PKT;
                int_keyscan_msg.u.buf = (void *)&Current_Key_Data;
                if (false == app_send_msg_to_apptask(&int_keyscan_msg))
                {
                    APP_PRINT_ERROR0("[io_keyscan] Keyscan_Handler: Send IO_MSG_KEYSCAN_RX_PKT failed!");
                    //Add user code here!
                    return;
                }
            }
            KeyScan_ClearINTPendingBit(KEYSCAN, KEYSCAN_INT_SCAN_END);
            KeyScan_INTMask(KEYSCAN, KEYSCAN_INT_SCAN_END, DISABLE);
        }
    }
    
    static void io_keyscan_handle_keys(T_IO_MSG *io_keyscan_msg)
    {
        uint16_t subtype = io_keyscan_msg->subtype;
    
        if (subtype == IO_MSG_KEYSCAN_RX_PKT)
        {
            KeyScan_Data_TypeDef *p_key_data = (KeyScan_Data_TypeDef *)io_keyscan_msg->u.buf;
            /* Single key press */
            if (p_key_data->length == 1)
            {
                APP_PRINT_INFO2("[io_keyscan] io_keyscan_handle_keys: Single key press. key: (%d, %d)",
                                p_key_data->key[0].row, p_key_data->key[0].column);
            }
    
            /* two keys press */
            if (p_key_data->length == 2)
            {
                APP_PRINT_INFO4("[io_keyscan] io_keyscan_handle_keys: Two key press. key0: (%d, %d), key1: (%d, %d)",
                                p_key_data->key[0].row, p_key_data->key[0].column, p_key_data->key[1].row,
                                p_key_data->key[1].column);
            }
        }
        else if (subtype == IO_MSG_KEYSCAN_ALLKEYRELEASE)
        {
            APP_PRINT_INFO0("[io_keyscan] io_keyscan_handle_keys: All keys release.");
    
        }
        else
        {
            APP_PRINT_INFO0("[io_keyscan] io_keyscan_handle_keys: Wrong key event!");
        }
    }
    
  3. 当 200ms 定时结束时,进入回调函数 timer_keyscan_callback ,检测 flag 标志位 Key_Pressed_Flag

    如果 flag 位为 true,表明当前有按键按下

    1. 将 flag 位置为 false。

    2. 执行 driver_keyboard_init ,重新初始化 Keyscan,使能 Keyscan 功能,重新启动键盘扫描。

    3. 执行 os_timer_restart() ,重启软件定时器,修改定时时间为 10ms,模拟按键松开的 debounce。

    如果 flag 位为 false,表明当前没有按键按下

    1. 定义消息子类型为 IO_MSG_KEYSCAN_ALLKEYRELEASE,在消息处理函数内打印按键全部松开信息。

    2. 执行 global_data_keyscan_init ,重置按键信息。

    3. 执行 driver_keyboard_init ,重新初始化 Keyscan。

    void timer_keyscan_callback(void *p_xTimer)
    {
        if (true == Key_Pressed_Flag)
        {
    //        APP_PRINT_INFO0("[io_keyscan] timer_keyscan_callback: start release timer");
            Key_Pressed_Flag = false;
            driver_keyboard_init(KeyScan_Debounce_Disable);
    
            /* Start timer to check key status */
            os_timer_restart(&p_xTimer, KEYSCAN_SW_RELEASE_TIMEOUT);
        }
        else
        {
            /* Keyscan release event detected */
    //        APP_PRINT_INFO0("[io_keyscan] timer_keyscan_callback: keyscan release event detected ");
            T_IO_MSG int_keyscan_msg;
            int_keyscan_msg.type = IO_MSG_TYPE_KEYSCAN;
            int_keyscan_msg.subtype = IO_MSG_KEYSCAN_ALLKEYRELEASE;
    
            if (false == app_send_msg_to_apptask(&int_keyscan_msg))
            {
                APP_PRINT_ERROR0("[io_keyscan] timer_keyscan_callback: Send IO_MSG_KEYSCAN_ALLKEYRELEASE failed!");
            }
    
            global_data_keyscan_init();
            driver_keyboard_init(KeyScan_Debounce_Enable);
        }
    }