commit 959699c1d31072947407d2c7d09ad0574aa65163 Author: Matthias Blankertz Date: Fri Mar 17 18:00:26 2023 +0100 Initial USB HID implementation - One button reported as gamepad button - One LED controlable via application specific HID report and FlyWithLua diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..583b74a --- /dev/null +++ b/.clang-format @@ -0,0 +1,118 @@ +# SPDX-License-Identifier: GPL-2.0 +# +# clang-format configuration file. Intended for clang-format >= 4. +# +# For more information, see: +# +# Documentation/process/clang-format.rst +# https://clang.llvm.org/docs/ClangFormat.html +# https://clang.llvm.org/docs/ClangFormatStyleOptions.html +# +--- +AccessModifierOffset: -4 +AlignAfterOpenBracket: Align +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +#AlignEscapedNewlines: Left # Unknown to clang-format-4.0 +AlignOperands: true +AlignTrailingComments: true +AllowAllParametersOfDeclarationOnNextLine: false +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: None +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: false +BinPackArguments: true +BinPackParameters: true +BraceWrapping: + AfterClass: false + AfterControlStatement: false + AfterEnum: false + AfterFunction: true + AfterNamespace: true + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + #AfterExternBlock: false # Unknown to clang-format-5.0 + BeforeCatch: false + BeforeElse: false + IndentBraces: false + #SplitEmptyFunction: true # Unknown to clang-format-4.0 + #SplitEmptyRecord: true # Unknown to clang-format-4.0 + #SplitEmptyNamespace: true # Unknown to clang-format-4.0 +BreakBeforeBinaryOperators: None +BreakBeforeBraces: Custom +#BreakBeforeInheritanceComma: false # Unknown to clang-format-4.0 +BreakBeforeTernaryOperators: false +BreakConstructorInitializersBeforeComma: false +#BreakConstructorInitializers: BeforeComma # Unknown to clang-format-4.0 +BreakAfterJavaFieldAnnotations: false +BreakStringLiterals: false +ColumnLimit: 120 +CommentPragmas: '^ IWYU pragma:' +#CompactNamespaces: false # Unknown to clang-format-4.0 +ConstructorInitializerAllOnOneLineOrOnePerLine: false +ConstructorInitializerIndentWidth: 8 +ContinuationIndentWidth: 8 +Cpp11BracedListStyle: false +DerivePointerAlignment: false +DisableFormat: false +ExperimentalAutoDetectBinPacking: false +#FixNamespaceComments: false # Unknown to clang-format-4.0 + +IncludeBlocks: Preserve +IncludeCategories: + - Regex: '.*' + Priority: 1 +IncludeIsMainRegex: '(Test)?$' +IndentCaseLabels: false +IndentPPDirectives: None +IndentWidth: 4 +IndentWrappedFunctionNames: false +JavaScriptQuotes: Leave +JavaScriptWrapImports: true +KeepEmptyLinesAtTheStartOfBlocks: false +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: Inner +#ObjCBinPackProtocolList: Auto # Unknown to clang-format-5.0 +ObjCBlockIndentWidth: 8 +ObjCSpaceAfterProperty: true +ObjCSpaceBeforeProtocolList: true + +# Taken from git's rules +#PenaltyBreakAssignment: 10 # Unknown to clang-format-4.0 +PenaltyBreakBeforeFirstCallParameter: 30 +PenaltyBreakComment: 10 +PenaltyBreakFirstLessLess: 0 +PenaltyBreakString: 10 +PenaltyExcessCharacter: 100 +PenaltyReturnTypeOnItsOwnLine: 60 + +PointerAlignment: Right +ReflowComments: false +SortIncludes: false +#SortUsingDeclarations: false # Unknown to clang-format-4.0 +SpaceAfterCStyleCast: false +SpaceAfterTemplateKeyword: true +SpaceBeforeAssignmentOperators: true +#SpaceBeforeCtorInitializerColon: true # Unknown to clang-format-5.0 +#SpaceBeforeInheritanceColon: true # Unknown to clang-format-5.0 +SpaceBeforeParens: ControlStatements +#SpaceBeforeRangeBasedForLoopColon: true # Unknown to clang-format-5.0 +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: false +SpacesInContainerLiterals: false +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +Standard: Cpp03 +TabWidth: 4 +UseTab: Never +... diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..567609b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +build/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..7733770 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "pico-sdk"] + path = pico-sdk + url = https://github.com/raspberrypi/pico-sdk.git diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..2983e2c --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,23 @@ +cmake_minimum_required(VERSION 3.13) + +# initialize pico-sdk from submodule +# note: this must happen before project() +include(pico-sdk/pico_sdk_init.cmake) + +project(rp2040_hid) + +# initialize the Raspberry Pi Pico SDK +pico_sdk_init() + +add_executable(rp2040_hid + src/main.c + src/usb_config.c +) + +# Add pico_stdlib library which aggregates commonly used features +target_link_libraries(rp2040_hid pico_stdlib pico_unique_id tinyusb_device tinyusb_board) +target_include_directories(rp2040_hid PRIVATE src) + + +# create map/bin/hex/uf2 file in addition to ELF. +pico_add_extra_outputs(rp2040_hid) diff --git a/FlyWithLua/rpi2040_hid.lua b/FlyWithLua/rpi2040_hid.lua new file mode 100644 index 0000000..fae7c81 --- /dev/null +++ b/FlyWithLua/rpi2040_hid.lua @@ -0,0 +1,20 @@ +device = hid_open(0x1209, 1) + +if device == nil then + print("No device!") +else + dataref("low_vac", "sim/cockpit2/annunciators/low_vacuum", "readable") + local prev_low_vac + function low_vac_indicator() + if low_vac ~= prev_low_vac then + if low_vac > 0 then + hid_write(device, 2, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) + else + hid_write(device, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) + end + prev_low_vac = low_vac + end + end + + do_every_frame("low_vac_indicator()") +end diff --git a/pico-sdk b/pico-sdk new file mode 160000 index 0000000..f396d05 --- /dev/null +++ b/pico-sdk @@ -0,0 +1 @@ +Subproject commit f396d05f8252d4670d4ea05c8b7ac938ef0cd381 diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..887ffff --- /dev/null +++ b/src/main.c @@ -0,0 +1,128 @@ +#include +#include +#include +#include +#include "usb_config.h" + +#define BUTTON1_GPIO 2 +#define LED1_GPIO 22 + +static void hid_task(void); + +// USB report structs +__attribute__((packed)) struct light_data { + uint8_t leds[16]; +}; + +__attribute__((packed)) struct button_report { + uint32_t buttons; +}; + +static struct light_data the_light_data; + +int main() +{ + setup_default_uart(); + tusb_init(); + + gpio_init(BUTTON1_GPIO); + gpio_set_dir(BUTTON1_GPIO, GPIO_IN); + gpio_pull_up(BUTTON1_GPIO); + + gpio_init(LED1_GPIO); + gpio_set_dir(LED1_GPIO, GPIO_OUT); + gpio_put(LED1_GPIO, true); + + printf("Hello, world!\n"); + while (1) { + tud_task(); + hid_task(); + } + return 0; +} + +static void hid_task(void) +{ + // Poll every 1ms + const uint32_t interval_ms = 1; + static uint32_t start_ms = 0; + + if (board_millis() - start_ms < interval_ms) + return; // poll interval not elapsed + start_ms += interval_ms; + + bool btn = !gpio_get(BUTTON1_GPIO); + + // Remote wakeup + if (tud_suspended() && btn) { + // Wake up host if we are in suspend mode + // and REMOTE_WAKEUP feature is enabled by host + tud_remote_wakeup(); + } + + if (tud_hid_ready()) { + struct button_report report = { .buttons = btn ? GAMEPAD_BUTTON_0 : 0 }; + tud_hid_n_report(ITF_NUM_HID, HID_REPORT_GAMEPAD, &report, sizeof(report)); + } +} + +void tud_hid_set_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, uint8_t const *buffer, + uint16_t bufsize) +{ + //printf("REPORT id %hhu type %d [0] %hhu size %hu\n", report_id, report_type, buffer[0], bufsize); + if (report_id == HID_REPORT_LIGHTS && report_type == HID_REPORT_TYPE_OUTPUT && + bufsize >= sizeof(struct light_data)) { + size_t i = 0; + memcpy(&the_light_data, buffer, sizeof(the_light_data)); + gpio_put(LED1_GPIO, the_light_data.leds[0] > 0x7f ? false : true); + } + + // echo back anything we received from host + tud_hid_report(0, buffer, bufsize); +} + +void tud_mount_cb(void) +{ +} + +// Invoked when device is unmounted +void tud_umount_cb(void) +{ +} + +// Invoked when usb bus is suspended +// remote_wakeup_en : if host allow us to perform remote wakeup +// Within 7ms, device must draw an average of current less than 2.5 mA from bus +void tud_suspend_cb(bool remote_wakeup_en) +{ + (void)remote_wakeup_en; +} + +// Invoked when usb bus is resumed +void tud_resume_cb(void) +{ +} + +uint16_t tud_hid_get_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, uint8_t *buffer, + uint16_t reqlen) +{ + size_t act_len; + printf("GET_REPORT id %hhu type %d reqlen %hu\n", report_id, report_type, buffer[0], reqlen); + if (report_type != HID_REPORT_TYPE_INPUT) + return 0; + switch (report_id) { + case HID_REPORT_LIGHTS: + act_len = sizeof(the_light_data) > reqlen ? reqlen : sizeof(the_light_data); + memcpy(buffer, &the_light_data, act_len); + return act_len; + case HID_REPORT_GAMEPAD: { + const bool btn = !gpio_get(BUTTON1_GPIO); + const struct button_report report = { .buttons = btn ? GAMEPAD_BUTTON_0 : 0 }; + act_len = sizeof(report) > reqlen ? reqlen : sizeof(report); + memcpy(buffer, &report, act_len); + return act_len; + } + default: + return 0; + } +} diff --git a/src/tusb_config.h b/src/tusb_config.h new file mode 100644 index 0000000..47b15ed --- /dev/null +++ b/src/tusb_config.h @@ -0,0 +1,93 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019 Ha Thach (tinyusb.org) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#ifndef _TUSB_CONFIG_H_ +#define _TUSB_CONFIG_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +//-------------------------------------------------------------------- +// COMMON CONFIGURATION +//-------------------------------------------------------------------- + +// defined by compiler flags for flexibility +#ifndef CFG_TUSB_MCU +#error CFG_TUSB_MCU must be defined +#endif + +#if CFG_TUSB_MCU == OPT_MCU_LPC18XX || CFG_TUSB_MCU == OPT_MCU_LPC43XX || CFG_TUSB_MCU == OPT_MCU_MIMXRT10XX || \ + CFG_TUSB_MCU == OPT_MCU_NUC505 || CFG_TUSB_MCU == OPT_MCU_CXD56 +#define CFG_TUSB_RHPORT0_MODE (OPT_MODE_DEVICE | OPT_MODE_HIGH_SPEED) +#else +#define CFG_TUSB_RHPORT0_MODE OPT_MODE_DEVICE +#endif + +#ifndef CFG_TUSB_OS +#define CFG_TUSB_OS OPT_OS_PICO +#endif + +// CFG_TUSB_DEBUG is defined by compiler in DEBUG build +// #define CFG_TUSB_DEBUG 0 + +/* USB DMA on some MCUs can only access a specific SRAM region with restriction on alignment. + * Tinyusb use follows macros to declare transferring memory so that they can be put + * into those specific section. + * e.g + * - CFG_TUSB_MEM SECTION : __attribute__ (( section(".usb_ram") )) + * - CFG_TUSB_MEM_ALIGN : __attribute__ ((aligned(4))) + */ +#ifndef CFG_TUSB_MEM_SECTION +#define CFG_TUSB_MEM_SECTION +#endif + +#ifndef CFG_TUSB_MEM_ALIGN +#define CFG_TUSB_MEM_ALIGN __attribute__((aligned(4))) +#endif + +//-------------------------------------------------------------------- +// DEVICE CONFIGURATION +//-------------------------------------------------------------------- + +#ifndef CFG_TUD_ENDPOINT0_SIZE +#define CFG_TUD_ENDPOINT0_SIZE 64 +#endif + +//------------- CLASS -------------// +#define CFG_TUD_CDC 0 +#define CFG_TUD_MSC 0 +#define CFG_TUD_HID 1 +#define CFG_TUD_MIDI 0 +#define CFG_TUD_VENDOR 0 + +// HID buffer size Should be sufficient to hold ID (if any) + Data +#define CFG_TUD_HID_BUFSIZE 32 + +#ifdef __cplusplus +} +#endif + +#endif /* _TUSB_CONFIG_H_ */ diff --git a/src/usb_config.c b/src/usb_config.c new file mode 100644 index 0000000..f0ad490 --- /dev/null +++ b/src/usb_config.c @@ -0,0 +1,159 @@ +#include "usb_config.h" + +#include +#include +#include +#include + +/* clang-format off */ +// Gamepad Report Descriptor Template with 32 buttons with following layout +// | Button Map (4 bytes) | +#define RP2040_HID_REPORT_DESC_GAMEPAD(...) \ + HID_USAGE_PAGE (HID_USAGE_PAGE_DESKTOP), \ + HID_USAGE(HID_USAGE_DESKTOP_GAMEPAD), \ + HID_COLLECTION (HID_COLLECTION_APPLICATION), \ + /* Report ID if any */ \ + __VA_ARGS__ \ + /* 32 bit Button Map */ \ + HID_USAGE_PAGE(HID_USAGE_PAGE_BUTTON), \ + HID_USAGE_MIN(1), \ + HID_USAGE_MAX(32), \ + HID_LOGICAL_MIN(0), \ + HID_LOGICAL_MAX(1), \ + HID_REPORT_COUNT(32), \ + HID_REPORT_SIZE(1), \ + HID_INPUT(HID_DATA | HID_VARIABLE | HID_ABSOLUTE), \ + HID_COLLECTION_END + +#define RP2040_HID_REPORT_DESC_LIGHTS(...) \ + HID_USAGE_PAGE(HID_USAGE_PAGE_DESKTOP), \ + HID_USAGE(0x00), \ + HID_COLLECTION(HID_COLLECTION_APPLICATION), \ + __VA_ARGS__ \ + HID_REPORT_COUNT(16), /*16 button lights */ \ + HID_REPORT_SIZE(8), \ + HID_LOGICAL_MIN(0x00), \ + HID_LOGICAL_MAX_N(0x00ff, 2), \ + HID_USAGE_PAGE(HID_USAGE_PAGE_ORDINAL), \ + HID_USAGE_MIN(1), \ + HID_USAGE_MAX(16), \ + HID_OUTPUT(HID_DATA | HID_VARIABLE | HID_ABSOLUTE), \ + HID_USAGE_MIN(1), \ + HID_USAGE_MAX(1), \ + HID_INPUT(HID_CONSTANT | HID_VARIABLE | HID_ABSOLUTE), \ + HID_COLLECTION_END + +/* clang-format on */ + +static char const *string_desc_arr[] = { + (const char[]){ 0x09, 0x04 }, // 0: is supported language is English (0x0409) + "blankertz.org", // 1: Manufacturer + "RP2040 HID", // 2: Product +}; + +static uint16_t _desc_str[32]; + +static char tohex(uint8_t x) +{ + return x < 0xa ? x + '0' : (x - 0xa) + 'A'; +} + +#define USBD_STR_SERIAL 3 + +// Invoked when received GET STRING DESCRIPTOR request +// Application return pointer to descriptor, whose contents must exist long enough for transfer to complete +uint16_t const *tud_descriptor_string_cb(uint8_t index, uint16_t langid) +{ + (void)langid; + + uint8_t chr_count; + + if (index == 0) { + memcpy(&_desc_str[1], string_desc_arr[0], 2); + chr_count = 1; + } else if (index == USBD_STR_SERIAL) { + pico_unique_board_id_t unique_id; + pico_get_unique_board_id(&unique_id); + for (int i = 0; i < PICO_UNIQUE_BOARD_ID_SIZE_BYTES; ++i) { + _desc_str[1 + i * 2] = tohex(unique_id.id[i] >> 4); + _desc_str[1 + i * 2 + 1] = tohex(unique_id.id[i] & 0xf); + } + chr_count = PICO_UNIQUE_BOARD_ID_SIZE_BYTES * 2; + } else { + // Convert ASCII string into UTF-16 + + if (!(index < sizeof(string_desc_arr) / sizeof(string_desc_arr[0]))) + return NULL; + + const char *str = string_desc_arr[index]; + + // Cap at max char + chr_count = strlen(str); + if (chr_count > 31) + chr_count = 31; + + for (uint8_t i = 0; i < chr_count; i++) { + _desc_str[1 + i] = str[i]; + } + } + + // first byte is length (including header), second byte is string type + _desc_str[0] = (TUSB_DESC_STRING << 8) | (2 * chr_count + 2); + + return _desc_str; +} + +static tusb_desc_device_t const desc_device = { .bLength = sizeof(tusb_desc_device_t), + .bDescriptorType = TUSB_DESC_DEVICE, + .bcdUSB = 0x0200, + .bDeviceClass = 0x00, + .bDeviceSubClass = 0x00, + .bDeviceProtocol = 0x00, + .bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE, + + .idVendor = 0x1209, + .idProduct = 0x0001, + .bcdDevice = 0x0100, + + .iManufacturer = 0x01, + .iProduct = 0x02, + .iSerialNumber = 0x03, + + .bNumConfigurations = 0x01 }; + +// Invoked when received GET DEVICE DESCRIPTOR +// Application return pointer to descriptor +uint8_t const *tud_descriptor_device_cb(void) +{ + return (uint8_t const *)&desc_device; +} + +static uint8_t const desc_hid_report[] = { RP2040_HID_REPORT_DESC_GAMEPAD(HID_REPORT_ID(HID_REPORT_GAMEPAD)), + RP2040_HID_REPORT_DESC_LIGHTS(HID_REPORT_ID(HID_REPORT_LIGHTS)) }; + +#define CONFIG_TOTAL_LEN (TUD_CONFIG_DESC_LEN + TUD_HID_DESC_LEN) + +#define EPNUM_HID 0x81 + +static uint8_t const desc_configuration[] = { + // Config number, interface count, string index, total length, attribute, power in mA + TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, CONFIG_TOTAL_LEN, TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP, 100), + + // Interface number, string index, protocol, report descriptor len, EP In & Out address, size & polling interval + TUD_HID_DESCRIPTOR(ITF_NUM_HID, 0, HID_ITF_PROTOCOL_NONE, sizeof(desc_hid_report), EPNUM_HID, CFG_TUD_HID_BUFSIZE, + 1) +}; + +// Invoked when received GET CONFIGURATION DESCRIPTOR +// Application return pointer to descriptor +// Descriptor contents must exist long enough for transfer to complete +uint8_t const *tud_descriptor_configuration_cb(uint8_t index) +{ + (void)index; // for multiple configurations + return desc_configuration; +} + +uint8_t const *tud_hid_descriptor_report_cb(uint8_t instance) +{ + return desc_hid_report; +} diff --git a/src/usb_config.h b/src/usb_config.h new file mode 100644 index 0000000..914567e --- /dev/null +++ b/src/usb_config.h @@ -0,0 +1,7 @@ +#pragma once + +#include + +enum { ITF_NUM_HID, ITF_NUM_TOTAL }; + +enum { HID_REPORT_GAMEPAD = 1, HID_REPORT_LIGHTS };