samples: usb: add HID keyboard sample

Add HID keyboard sample for the new experimental USB device support.
This is a limited and not fully compliant HID keyboard implementation.

Signed-off-by: Johann Fischer <johann.fischer@nordicsemi.no>
This commit is contained in:
Johann Fischer 2023-12-04 11:43:29 +01:00 committed by Anas Nashif
commit 859cc2f1fe
10 changed files with 408 additions and 0 deletions

View file

@ -0,0 +1,9 @@
# SPDX-License-Identifier: Apache-2.0
cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(hid-keyboard)
include(${ZEPHYR_BASE}/samples/subsys/usb/common/common.cmake)
FILE(GLOB app_sources src/*.c)
target_sources(app PRIVATE ${app_sources})

View file

@ -0,0 +1,9 @@
# Copyright (c) 2023 Nordic Semiconductor ASA
# SPDX-License-Identifier: Apache-2.0
# Source common USB sample options used to initialize new experimental USB
# device stack. The scope of these options is limited to USB samples in project
# tree, you cannot use them in your own application.
source "samples/subsys/usb/common/Kconfig.sample_usbd"
source "Kconfig.zephyr"

View file

@ -0,0 +1,36 @@
.. zephyr:code-sample:: usb-hid-keyboard
:name: USB HID keyboard
:relevant-api: usbd_api usbd_hid_class input_interface
Implement a basic HID keyboard device.
Overview
********
This sample application demonstrates the HID keyboard implementation using the
new experimental USB device stack.
Requirements
************
This project requires an experimental USB device driver (UDC API) and uses the
:ref:`input` API. There must be a :dtcompatible:`gpio-keys` group of buttons
or keys defined at the board level that can generate input events.
At least one key is required and up to four can be used. The first three keys
are used for Num Lock, Caps Lock and Scroll Lock. The fourth key is used to
report HID keys 1, 2, 3 and the right Alt modifier at once.
The example can use up to three LEDs, configured via the devicetree alias such
as ``led0``, to indicate the state of the keyboard LEDs.
Building and Running
********************
This sample can be built for multiple boards, in this example we will build it
for the nRF52840DK board:
.. zephyr-app-commands::
:zephyr-app: samples/subsys/usb/hid-keyboard
:board: nrf52840dk/nrf52840
:goals: build flash
:compact:

View file

@ -0,0 +1,15 @@
/*
* Copyright (c) 2023 Nordic Semiconductor ASA
*
* SPDX-License-Identifier: Apache-2.0
*/
/ {
hid_dev_0: hid_dev_0 {
compatible = "zephyr,hid-device";
interface-name = "HID0";
protocol-code = "keyboard";
in-report-size = <64>;
in-polling-rate = <1000>;
};
};

View file

@ -0,0 +1,14 @@
/*
* Copyright (c) 2023 Nordic Semiconductor ASA
*
* SPDX-License-Identifier: Apache-2.0
*/
/ {
hid_dev_0: hid_dev_0 {
compatible = "zephyr,hid-device";
interface-name = "HID0";
in-report-size = <256>;
in-polling-rate = <1000>;
};
};

View file

@ -0,0 +1,15 @@
/*
* Copyright (c) 2023 Nordic Semiconductor ASA
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "large_in_report.overlay"
/ {
hid_dev_0: hid_dev_0 {
compatible = "zephyr,hid-device";
out-report-size = <128>;
out-polling-rate = <16000>;
};
};

View file

@ -0,0 +1,15 @@
/*
* Copyright (c) 2023 Nordic Semiconductor ASA
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "app.overlay"
/ {
hid_dev_0: hid_dev_0 {
compatible = "zephyr,hid-device";
out-report-size = <64>;
out-polling-rate = <16000>;
};
};

View file

@ -0,0 +1,12 @@
CONFIG_USB_DEVICE_STACK_NEXT=y
CONFIG_USBD_HID_SUPPORT=y
CONFIG_LOG=y
CONFIG_USBD_LOG_LEVEL_WRN=y
CONFIG_USBD_HID_LOG_LEVEL_WRN=y
CONFIG_UDC_DRIVER_LOG_LEVEL_WRN=y
CONFIG_SAMPLE_USBD_PID=0x0007
CONFIG_GPIO=y
CONFIG_INPUT=y
CONFIG_INPUT_MODE_SYNCHRONOUS=y

View file

@ -0,0 +1,26 @@
sample:
name: USB HID keyboard sample
common:
harness: button
filter: dt_alias_exists("sw0") and dt_alias_exists("led0")
depends_on:
- usb_device
- gpio
platform_allow:
- nrf52840dk/nrf52840
- frdm_k64f
tests:
sample.usbd.hid-keyboard:
tags: usb
sample.usbd.hid-keyboard.out-report:
tags: usb
extra_args:
- EXTRA_DTC_OVERLAY_FILE="out_report.overlay"
sample.usbd.hid-keyboard.large-report:
tags: usb
extra_args:
- EXTRA_DTC_OVERLAY_FILE="large_in_report.overlay"
sample.usbd.hid-keyboard.large-out-report:
tags: usb
extra_args:
- EXTRA_DTC_OVERLAY_FILE="large_out_report.overlay"

View file

@ -0,0 +1,257 @@
/*
* Copyright (c) 2024 Nordic Semiconductor ASA
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <sample_usbd.h>
#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/input/input.h>
#include <zephyr/usb/usbd.h>
#include <zephyr/usb/class/usbd_hid.h>
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(main, LOG_LEVEL_INF);
static const uint8_t hid_report_desc[] = HID_KEYBOARD_REPORT_DESC();
enum kb_leds_idx {
KB_LED_NUMLOCK = 0,
KB_LED_CAPSLOCK,
KB_LED_SCROLLLOCK,
KB_LED_COUNT,
};
static const struct gpio_dt_spec kb_leds[KB_LED_COUNT] = {
GPIO_DT_SPEC_GET_OR(DT_ALIAS(led0), gpios, {0}),
GPIO_DT_SPEC_GET_OR(DT_ALIAS(led1), gpios, {0}),
GPIO_DT_SPEC_GET_OR(DT_ALIAS(led2), gpios, {0}),
};
enum kb_report_idx {
KB_MOD_KEY = 0,
KB_RESERVED,
KB_KEY_CODE1,
KB_KEY_CODE2,
KB_KEY_CODE3,
KB_KEY_CODE4,
KB_KEY_CODE5,
KB_KEY_CODE6,
KB_REPORT_COUNT,
};
struct kb_event {
uint16_t code;
int32_t value;
};
K_MSGQ_DEFINE(kb_msgq, sizeof(struct kb_event), 2, 1);
static uint8_t __aligned(sizeof(void *)) report[KB_REPORT_COUNT];
static uint32_t kb_duration;
static bool kb_ready;
static void input_cb(struct input_event *evt)
{
struct kb_event kb_evt;
kb_evt.code = evt->code;
kb_evt.value = evt->value;
if (k_msgq_put(&kb_msgq, &kb_evt, K_NO_WAIT) != 0) {
LOG_ERR("Failed to put new input event");
}
}
INPUT_CALLBACK_DEFINE(NULL, input_cb);
static void kb_iface_ready(const struct device *dev, const bool ready)
{
LOG_INF("HID device %s interface is %s",
dev->name, ready ? "ready" : "not ready");
kb_ready = ready;
}
static int kb_get_report(const struct device *dev,
const uint8_t type, const uint8_t id, const uint16_t len,
uint8_t *const buf)
{
LOG_WRN("Get Report not implemented, Type %u ID %u", type, id);
return 0;
}
static int kb_set_report(const struct device *dev,
const uint8_t type, const uint8_t id, const uint16_t len,
const uint8_t *const buf)
{
if (type != HID_REPORT_TYPE_OUTPUT) {
LOG_WRN("Unsupported report type");
return -ENOTSUP;
}
for (unsigned int i = 0; i < ARRAY_SIZE(kb_leds); i++) {
if (kb_leds[i].port == NULL) {
continue;
}
(void)gpio_pin_set_dt(&kb_leds[i], buf[0] & BIT(i));
}
return 0;
}
/* Idle duration is stored but not used to calculate idle reports. */
static void kb_set_idle(const struct device *dev,
const uint8_t id, const uint32_t duration)
{
LOG_INF("Set Idle %u to %u", id, duration);
kb_duration = duration;
}
static uint32_t kb_get_idle(const struct device *dev, const uint8_t id)
{
LOG_INF("Get Idle %u to %u", id, kb_duration);
return kb_duration;
}
static void kb_set_protocol(const struct device *dev, const uint8_t proto)
{
LOG_INF("Protocol changed to %s",
proto == 0U ? "Boot Protocol" : "Report Protocol");
}
static void kb_output_report(const struct device *dev, const uint16_t len,
const uint8_t *const buf)
{
LOG_HEXDUMP_DBG(buf, len, "o.r.");
kb_set_report(dev, HID_REPORT_TYPE_OUTPUT, 0U, len, buf);
}
struct hid_device_ops kb_ops = {
.iface_ready = kb_iface_ready,
.get_report = kb_get_report,
.set_report = kb_set_report,
.set_idle = kb_set_idle,
.get_idle = kb_get_idle,
.set_protocol = kb_set_protocol,
.output_report = kb_output_report,
};
int main(void)
{
struct usbd_contex *sample_usbd;
const struct device *hid_dev;
int ret;
for (unsigned int i = 0; i < ARRAY_SIZE(kb_leds); i++) {
if (kb_leds[i].port == NULL) {
continue;
}
if (!gpio_is_ready_dt(&kb_leds[i])) {
LOG_ERR("LED device %s is not ready", kb_leds[i].port->name);
return -EIO;
}
ret = gpio_pin_configure_dt(&kb_leds[i], GPIO_OUTPUT_INACTIVE);
if (ret != 0) {
LOG_ERR("Failed to configure the LED pin, %d", ret);
return -EIO;
}
}
hid_dev = DEVICE_DT_GET_ONE(zephyr_hid_device);
if (!device_is_ready(hid_dev)) {
LOG_ERR("HID Device is not ready");
return -EIO;
}
ret = hid_device_register(hid_dev,
hid_report_desc, sizeof(hid_report_desc),
&kb_ops);
if (ret != 0) {
LOG_ERR("Failed to register HID Device, %d", ret);
return ret;
}
sample_usbd = sample_usbd_init_device(NULL);
if (sample_usbd == NULL) {
LOG_ERR("Failed to initialize USB device");
return -ENODEV;
}
ret = usbd_enable(sample_usbd);
if (ret) {
LOG_ERR("Failed to enable device support");
return ret;
}
LOG_INF("HID keyboard sample is initialized");
while (true) {
struct kb_event kb_evt;
k_msgq_get(&kb_msgq, &kb_evt, K_FOREVER);
switch (kb_evt.code) {
case INPUT_KEY_0:
if (kb_evt.value) {
report[KB_KEY_CODE1] = HID_KEY_NUMLOCK;
} else {
report[KB_KEY_CODE1] = 0;
}
break;
case INPUT_KEY_1:
if (kb_evt.value) {
report[KB_KEY_CODE2] = HID_KEY_CAPSLOCK;
} else {
report[KB_KEY_CODE2] = 0;
}
break;
case INPUT_KEY_2:
if (kb_evt.value) {
report[KB_KEY_CODE3] = HID_KEY_SCROLLLOCK;
} else {
report[KB_KEY_CODE3] = 0;
}
break;
case INPUT_KEY_3:
if (kb_evt.value) {
report[KB_MOD_KEY] = HID_KBD_MODIFIER_RIGHT_ALT;
report[KB_KEY_CODE4] = HID_KEY_1;
report[KB_KEY_CODE5] = HID_KEY_2;
report[KB_KEY_CODE6] = HID_KEY_3;
} else {
report[KB_MOD_KEY] = HID_KBD_MODIFIER_NONE;
report[KB_KEY_CODE4] = 0;
report[KB_KEY_CODE5] = 0;
report[KB_KEY_CODE6] = 0;
}
break;
default:
LOG_INF("Unrecognized input code %u value %d",
kb_evt.code, kb_evt.value);
continue;
}
if (!kb_ready) {
LOG_INF("USB HID device is not ready");
continue;
}
ret = hid_device_submit_report(hid_dev, sizeof(report), report);
if (ret) {
LOG_ERR("HID submit report error, %d", ret);
}
}
return 0;
}