From 19c0789107038d74220b9e2ac791675e58011e8e Mon Sep 17 00:00:00 2001 From: HASUMI Hitoshi <hasumikin@gmail.com> Date: Wed, 8 Sep 2021 00:33:40 +0900 Subject: [PATCH 01/10] WIP --- CMakeLists.txt | 30 ++- include/tusb_config.h | 15 +- src/main.c | 5 +- src/msc_disk.c | 418 +++++++++++++++++++++++++++++++++ src/msc_disk.h | 9 + src/ruby/lib/keyboard.rb | 4 + src/ruby/lib/rgb.rb | 3 +- src/ruby/lib/rgb_task.rb | 2 + src/ruby/lib/rotary_encoder.rb | 3 +- src/ruby/lib/tud.rb | 11 +- src/ruby/sig/object.rbs | 1 + src/usb.c | 40 ++-- src/usb_descriptors.c | 257 ++++++++++++++++++++ src/version.h | 5 + src/version.h.in | 5 + 15 files changed, 774 insertions(+), 34 deletions(-) create mode 100644 src/msc_disk.c create mode 100644 src/msc_disk.h create mode 100644 src/usb_descriptors.c create mode 100644 src/version.h create mode 100644 src/version.h.in diff --git a/CMakeLists.txt b/CMakeLists.txt index 26bfe9bb..981e045b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,12 +11,25 @@ add_definitions(-DMRBC_USE_HAL_RP2040 -DMRBC_REQUIRE_32BIT_ALIGNMENT -DMAX_REGS_ # initialize the Raspberry Pi Pico SDK pico_sdk_init() -# rest of your project +##################################################### +# project specific configuration from here + +execute_process (COMMAND date +%Y%m%d OUTPUT_VARIABLE CMAKE_BUILDDATE OUTPUT_STRIP_TRAILING_WHITESPACE) +execute_process (COMMAND git rev-parse --short HEAD OUTPUT_VARIABLE CMAKE_REVISION OUTPUT_STRIP_TRAILING_WHITESPACE) +set (PRK_VERSION 0.9.0) +set (PRK_BUILDDATE ${CMAKE_BUILDDATE}) +set (PRK_REVISION ${CMAKE_REVISION}) +configure_file ("${CMAKE_CURRENT_SOURCE_DIR}/src/version.h.in" "${CMAKE_CURRENT_SOURCE_DIR}/src/version.h") + +set(PROJECT "prk_firmware-${PRK_VERSION}-${PRK_BUILDDATE}-${PRK_REVISION}") +#set(PROJECT "prk_firmware") # for test add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/lib) -add_executable(prk_firmware +add_executable(${PROJECT} src/main.c + src/msc_disk.c + src/usb_descriptors.c src/gpio.c src/usb.c src/uart.c @@ -25,8 +38,8 @@ add_executable(prk_firmware lib/picoruby/cli/sandbox.c ) -pico_generate_pio_header(prk_firmware ${CMAKE_CURRENT_LIST_DIR}/src/uart_tx.pio) -pico_generate_pio_header(prk_firmware ${CMAKE_CURRENT_LIST_DIR}/src/ws2812.pio) +pico_generate_pio_header(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/src/uart_tx.pio) +pico_generate_pio_header(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/src/ws2812.pio) set(RBC ${CMAKE_CURRENT_SOURCE_DIR}/lib/picoruby/build/bin/host-production/alloc_libc/picorbc) #set(RBC RBENV_VERSION=mruby-3.0.0 mrbc) @@ -59,13 +72,13 @@ add_dependencies(ruby steep ) -add_dependencies(prk_firmware +add_dependencies(${PROJECT} picoruby ruby ruby_2 ) -target_link_libraries(prk_firmware +target_link_libraries(${PROJECT} pico_stdlib tinyusb_device tinyusb_board @@ -73,9 +86,10 @@ target_link_libraries(prk_firmware hardware_pio hardware_pwm pico_multicore + hardware_flash ) -include_directories(prk_firmware +include_directories(${PROJECT} PRIVATE ${CMAKE_CURRENT_LIST_DIR} ${CMAKE_CURRENT_LIST_DIR}/include @@ -83,4 +97,4 @@ include_directories(prk_firmware ) # create map/bin/hex/uf2 file in addition to ELF. -pico_add_extra_outputs(prk_firmware) +pico_add_extra_outputs(${PROJECT}) diff --git a/include/tusb_config.h b/include/tusb_config.h index 1bb0b443..b73bc7d6 100644 --- a/include/tusb_config.h +++ b/include/tusb_config.h @@ -94,8 +94,8 @@ #endif //------------- CLASS -------------// -#define CFG_TUD_CDC 0 -#define CFG_TUD_MSC 0 +#define CFG_TUD_CDC 1 +#define CFG_TUD_MSC 1 #define CFG_TUD_HID 1 #define CFG_TUD_MIDI 0 #define CFG_TUD_VENDOR 0 @@ -103,6 +103,17 @@ // HID buffer size Should be sufficient to hold ID (if any) + Data #define CFG_TUD_HID_EP_BUFSIZE 64 +// It is normaly 512, but we use 4096 to avoid cache coherency problem +// on writing flash +#define CFG_TUD_MSC_EP_BUFSIZE 512 + +// CDC FIFO size of TX and RX +#define CFG_TUD_CDC_RX_BUFSIZE (TUD_OPT_HIGH_SPEED ? 512 : 64) +#define CFG_TUD_CDC_TX_BUFSIZE (TUD_OPT_HIGH_SPEED ? 512 : 64) + +// CDC Endpoint transfer buffer size, more is faster +#define CFG_TUD_CDC_EP_BUFSIZE (TUD_OPT_HIGH_SPEED ? 512 : 64) + #ifdef __cplusplus } #endif diff --git a/src/main.c b/src/main.c index cbeb1508..84c7f707 100644 --- a/src/main.c +++ b/src/main.c @@ -9,6 +9,7 @@ #include "hardware/clocks.h" /* mrbc_class */ +#include "msc_disk.h" #include "gpio.h" #include "usb.h" #include "uart.h" @@ -46,7 +47,7 @@ c_rand(mrb_vm *vm, mrb_value *v, int argc) SET_INT_RETURN(rand()); } -#define MEMORY_SIZE (1024*220) +#define MEMORY_SIZE (1024*200) static uint8_t memory_pool[MEMORY_SIZE]; @@ -73,9 +74,11 @@ int main() { stdio_init_all(); board_init(); tusb_init(); + msc_init(); mrbc_init(memory_pool, MEMORY_SIZE); mrbc_define_method(0, mrbc_class_object, "board_millis", c_board_millis); mrbc_define_method(0, mrbc_class_object, "rand", c_rand); + MSC_INIT(); GPIO_INIT(); USB_INIT(); UART_INIT(); diff --git a/src/msc_disk.c b/src/msc_disk.c new file mode 100644 index 00000000..ad7c418b --- /dev/null +++ b/src/msc_disk.c @@ -0,0 +1,418 @@ +/* + * 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. + * + */ + +#include "bsp/board.h" +#include "tusb.h" + +#if CFG_TUD_MSC + +#include "hardware/flash.h" +#include "hardware/sync.h" + +#include "msc_disk.h" + +#define FLASH_TARGET_OFFSET 0x00180000 /* 1.5MB */ +#define FLASH_MMAP_ADDR (XIP_BASE + FLASH_TARGET_OFFSET) /* 0x10180000 */ +#define SECTOR_SIZE 4096 +#define SECTOR_COUNT 128 /* 4096 * 128 = 512KB */ + +// whether host does safe-eject +static bool ejected = false; + +#include "version.h" + +#define PRK_DESCRIPTION \ +"PRK Firmware " PRK_VERSION " (" PRK_BUILDDATE " revision " PRK_REVISION ")" + +/* PRK_DESCRIPTION should be located at the beginning of README. see msc_init() */ +#define README_CONTENTS \ +PRK_DESCRIPTION "\n\nWelcome to PRK Firmware!\n\n\ +Usage:\n\ +- Drag and drop your `keymap.rb` into this directory\n\ +- Then, your keyboard will be automatically rebooted. That's all!\n\n\ +Notice:\n\ +- Make sure you always have a backup of your `keymap.rb`\n\ + because upgrading prk_firmware-*.uf2 will remove it from flash\n\n\ +https://github.com/picoruby/prk_firmware\n" + +#define README_LENGTH (sizeof(README_CONTENTS) - 1) + +uint8_t msc_disk[4][SECTOR_SIZE] = +{ + //------------- Block0: Boot Sector -------------// + // byte_per_sector = SECTOR_SIZE; fat12_sector_num_16 = SECTOR_COUNT; + // sector_per_cluster = 1; reserved_sectors = 1; + // fat_num = 1; fat12_root_entry_num = 16; + // sector_per_fat = 1; sector_per_track = 1; head_num = 1; hidden_sectors = 0; + // drive_number = 0x80; media_type = 0xf8; extended_boot_signature = 0x29; + // filesystem_type = "FAT12 "; volume_serial_number = 0x1234; volume_label = "PRKFirmware"; + { + 0xEB, 0x3C, 0x90, /* BS_jmpBoot */ + 0x4D, 0x53, 0x44, 0x4F, 0x53, 0x35, 0x2E, 0x30, /* BS_OEMName "MSDOS5.0" */ + 0x00, 0x10, /* ** BPB_BytePerSec ** */ + 0x01, /* BPB_SecPerClus */ + 0x01, 0x00, /* BPB_RsvdSecCnt */ + 0x01, /* BPB_NumFATs */ + 0x80, 0x00, /* ** BPB_RootEntCnt ** */ + 0x00, 0x01, /* ** BPB_TotSec16 ** */ + 0xF8, /* BPB_Media */ + 0x01, 0x00, /* BPB_FATSz16 */ + 0x01, 0x00, /* BPB_SecPerTrk */ + 0x01, 0x00, /* BPB_NumHeads */ + 0x00, 0x00, 0x00, 0x00, /* BPB_HiddSec */ + 0x00, 0x00, 0x00, 0x00, /* BPB_TotSec32 */ + 0x80, 0x00, 0x29, 0x34, 0x12, 0x00, 0x00, 'P' , 'R' , 'K' , 'F' , 'i' , + 'r' , 'm' , 'w' , 'a' , 'r' , 'e' , 0x46, 0x41, 0x54, 0x31, 0x32, 0x20, 0x20, 0x20, 0x00, 0x00, + // Zerofill until BS_BootSign (0xAA55 at offset 510) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0xAA + }, + + //------------- Block1: FAT12 Table -------------// + { + 0xF8, 0xFF, 0xFF, 0xFF, 0x0F // // first 2 entries must be F8FF, third entry is cluster end of readme file + }, + + //------------- Block2: Root Directory -------------// + { + // first entry is volume label + 'P' , 'R' , 'K' , 'F' , 'i' , 'r' , 'm' , 'w' , 'a' , 'r' , 'e' , 0x08, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4F, 0x6D, 0x65, 0x43, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // second entry is readme file + 'R' , 'E' , 'A' , 'D' , 'M' , 'E' , ' ' , ' ' , 'T' , 'X' , 'T' , 0x20, 0x00, 0xC6, 0x52, 0x6D, + 0x65, 0x43, 0x65, 0x43, 0x00, 0x00, 0x88, 0x6D, 0x65, 0x43, 0x02, 0x00, + // readme's files size (4 Bytes little endian) + README_LENGTH & 0xFF, + README_LENGTH >> 8 & 0xFF, + README_LENGTH >> 16 & 0xFF, + README_LENGTH >> 24 + }, + + //------------- Block3: Readme Content -------------// + README_CONTENTS +}; + +// Invoked when received SCSI_CMD_INQUIRY +// Application fill vendor id, product id and revision with string up to 8, 16, 4 characters respectively +void tud_msc_inquiry_cb(uint8_t lun, uint8_t vendor_id[8], uint8_t product_id[16], uint8_t product_rev[4]) +{ + (void) lun; + + const char vid[] = "TinyUSB"; + const char pid[] = "Mass Storage"; + const char rev[] = "1.0"; + + memcpy(vendor_id , vid, strlen(vid)); + memcpy(product_id , pid, strlen(pid)); + memcpy(product_rev, rev, strlen(rev)); +} + +// Invoked when received Test Unit Ready command. +// return true allowing host to read/write this LUN e.g SD card inserted +bool tud_msc_test_unit_ready_cb(uint8_t lun) +{ + (void) lun; + + // RAM disk is ready until ejected + if (ejected) { + tud_msc_set_sense(lun, SCSI_SENSE_NOT_READY, 0x3a, 0x00); + return false; + } + + return true; +} + +// Invoked when received SCSI_CMD_READ_CAPACITY_10 and SCSI_CMD_READ_FORMAT_CAPACITY to determine the disk size +// Application update block count and block size +void tud_msc_capacity_cb(uint8_t lun, uint32_t* block_count, uint16_t* block_size) +{ + (void) lun; + + *block_count = SECTOR_COUNT; + *block_size = SECTOR_SIZE; +} + +// Invoked when received Start Stop Unit command +// - Start = 0 : stopped power mode, if load_eject = 1 : unload disk storage +// - Start = 1 : active mode, if load_eject = 1 : load disk storage +bool tud_msc_start_stop_cb(uint8_t lun, uint8_t power_condition, bool start, bool load_eject) +{ + (void) lun; + (void) power_condition; + + if ( load_eject ) + { + if (start) + { + // load disk storage + ejected = false; + }else + { + // unload disk storage + ejected = true; + } + } + + return true; +} + +// Callback invoked when received READ10 command. +// Copy disk's data to buffer (up to bufsize) and return number of copied bytes. +int32_t tud_msc_read10_cb(uint8_t lun, uint32_t lba, uint32_t offset, void* buffer, uint32_t bufsize) +{ + (void) lun; + // out of ramdisk + if ( lba >= SECTOR_COUNT ) return -1; + memcpy(buffer, (void *)(FLASH_MMAP_ADDR + lba * SECTOR_SIZE + offset), bufsize); + return bufsize; +} + +bool tud_msc_is_writable_cb (uint8_t lun) +{ + (void) lun; +#ifdef PICORUBY_MSC_READONLY + return false; +#else + return true; +#endif +} + +// Callback invoked when received WRITE10 command. +// Process data in buffer to disk's storage and return number of written bytes +int32_t tud_msc_write10_cb(uint8_t lun, uint32_t lba, uint32_t offset, uint8_t* buffer, uint32_t bufsize) +/* + * TODO: Cache coherency problem can be avoided if bufsize == SECTOR_SIZE(4094) + */ +{ + (void) lun; + // out of ramdisk + if ( lba >= SECTOR_COUNT ) return -1; + uint32_t ints = save_and_disable_interrupts(); + if (offset == 0) { + flash_range_erase(FLASH_TARGET_OFFSET + lba * SECTOR_SIZE, SECTOR_SIZE); + } + flash_range_program(FLASH_TARGET_OFFSET + lba * SECTOR_SIZE + offset, buffer, bufsize); + restore_interrupts(ints); + return bufsize; +} + +// Callback invoked when received an SCSI command not in built-in list below +// - READ_CAPACITY10, READ_FORMAT_CAPACITY, INQUIRY, MODE_SENSE6, REQUEST_SENSE +// - READ10 and WRITE10 has their own callbacks +int32_t tud_msc_scsi_cb (uint8_t lun, uint8_t const scsi_cmd[16], void* buffer, uint16_t bufsize) +{ + // read10 & write10 has their own callback and MUST not be handled here + + void const* response = NULL; + int32_t resplen = 0; + + // most scsi handled is input + bool in_xfer = true; + + switch (scsi_cmd[0]) + { + case SCSI_CMD_PREVENT_ALLOW_MEDIUM_REMOVAL: + // Host is about to read/write etc ... better not to disconnect disk + resplen = 0; + break; + + default: + // Set Sense = Invalid Command Operation + tud_msc_set_sense(lun, SCSI_SENSE_ILLEGAL_REQUEST, 0x20, 0x00); + + // negative means error -> tinyusb could stall and/or response with failed status + resplen = -1; + break; + } + + // return resplen must not larger than bufsize + if ( resplen > bufsize ) resplen = bufsize; + + if ( response && (resplen > 0) ) + { + if(in_xfer) + { + memcpy(buffer, response, resplen); + }else + { + // SCSI output + } + } + + return resplen; +} + +void msc_init(void) +{ +#ifndef FORCE_FORMAT_FLASH + if ( + strncmp( + (void *)(FLASH_MMAP_ADDR + SECTOR_SIZE * 3), + PRK_DESCRIPTION, + sizeof(PRK_DESCRIPTION) - 1 /* Skips null term */ + ) + ) +#endif + { + /* These functions know XIP_BASE */ + flash_range_erase(FLASH_TARGET_OFFSET, SECTOR_SIZE * 4); + flash_range_program(FLASH_TARGET_OFFSET, msc_disk[0], SECTOR_SIZE * 4); + } +} + +/* + * Directory entry in FAT Volume + * (FAT data is little endian) + */ +typedef struct dir_ent { + char Name[11]; + uint8_t Attr; + uint8_t NTRes; + uint8_t CrtTimeTenth; + char CrtTime[2]; + char CrtDate[2]; + char LastAccDate[2]; + uint16_t FstClusHI; + char WrtTime[2]; + char WrtDate[2]; + uint16_t FstClusLO; + uint32_t FileSize; +} DirEnt; + +/* + * Only works in top directory + * Only works if SFN (short file name) + */ +void +msc_findDirEnt(const char *filename, DirEnt *entry) +{ + void *addr; + for ( + addr = (void *)(FLASH_MMAP_ADDR + (SECTOR_SIZE * 2) + 32); + (uint32_t)addr < FLASH_MMAP_ADDR + SECTOR_SIZE * SECTOR_COUNT; + addr += 32 + ) + { + entry->Name[0] = '\0'; + memcpy(entry, addr, 32); + if (entry->Name[0] == 0xe5) continue; + if (entry->Name[0] == '\0') return; + if (strncmp(filename, entry->Name, 11) == 0 && entry->Attr == 0x20) return; + } +} + +//volatile char ppp; +void +tud_msc_write10_complete_cb(uint8_t lun) +{ + (void)lun; +// DirEnt entry; +// msc_findDirEnt("README TXT", &entry); +// if (entry.Name[0] != '\0') { +// const char *program = (const char *)(FLASH_MMAP_ADDR + SECTOR_SIZE * (1 + entry.FstClusLO)); +// ppp = program[0]; +// } +} + +//--------------------------------------------------------------------+ +// USB CDC +//--------------------------------------------------------------------+ +void +c_cdc_task(mrb_vm *vm, mrb_value *v, int argc) +{ + // connected() check for DTR bit + // Most but not all terminal client set this when making connection + // if ( tud_cdc_connected() ) + { + // connected and there are data available + if ( tud_cdc_available() ) + { + // read datas + char buf[64]; + uint32_t count = tud_cdc_read(buf, sizeof(buf)); + (void) count; + + // Echo back + // Note: Skip echo by commenting out write() and write_flush() + // for throughput test e.g + // $ dd if=/dev/zero of=/dev/ttyACM0 count=10000 + tud_cdc_write(buf, count); + tud_cdc_write_flush(); + } + } +} + +// Invoked when cdc when line state changed e.g connected/disconnected +void tud_cdc_line_state_cb(uint8_t itf, bool dtr, bool rts) +{ + (void) itf; + (void) rts; + + // TODO set some indicator + if ( dtr ) + { + // Terminal connected + }else + { + // Terminal disconnected + } +} + +// Invoked when CDC interface received data from host +void tud_cdc_rx_cb(uint8_t itf) +{ + (void) itf; +} + +#endif diff --git a/src/msc_disk.h b/src/msc_disk.h new file mode 100644 index 00000000..8e810e69 --- /dev/null +++ b/src/msc_disk.h @@ -0,0 +1,9 @@ +/* mruby/c VM */ +#include <mrubyc.h> + +void msc_init(void); +void c_cdc_task(mrb_vm *vm, mrb_value *v, int argc); + +#define MSC_INIT() do { \ + mrbc_define_method(0, mrbc_class_object, "cdc_task", c_cdc_task); \ +} while (0) diff --git a/src/ruby/lib/keyboard.rb b/src/ruby/lib/keyboard.rb index a2ebca7e..e17b4d41 100644 --- a/src/ruby/lib/keyboard.rb +++ b/src/ruby/lib/keyboard.rb @@ -335,6 +335,7 @@ class Keyboard letter = nil def initialize + puts "Initializing Keyboard ..." # mruby/c VM doesn't work with a CONSTANT to make another CONSTANT # steep doesn't allow dynamic assignment of CONSTANT @SHIFT_LETTER_THRESHOLD_A = LETTER.index('A').to_i @@ -624,6 +625,8 @@ def send_key(symbol) # Please refrain from "refactoring" for a while. # ************************************************************** def start! + puts "Starting keyboard task ..." + start_features @keycodes = Array.new # To avoid unintentional report on startup @@ -871,6 +874,7 @@ def unlock_layer end def macro(text, opts = [:ENTER]) + puts "macro: #{text}" prev_c = "" text.to_s.each_char do |c| index = LETTER.index(c) diff --git a/src/ruby/lib/rgb.rb b/src/ruby/lib/rgb.rb index 58f27acf..def899d2 100644 --- a/src/ruby/lib/rgb.rb +++ b/src/ruby/lib/rgb.rb @@ -1,5 +1,6 @@ class RGB - def initialize(pin, underglow_size, backlight_size, is_rgbw) + def initialize(pin, underglow_size, backlight_size, is_rgbw) + puts "Initializing RGB ..." @fifo = Array.new # TODO: @underglow_size, @backlight_size @pixel_size = underglow_size + backlight_size diff --git a/src/ruby/lib/rgb_task.rb b/src/ruby/lib/rgb_task.rb index bd0fb2db..d1d8c13d 100644 --- a/src/ruby/lib/rgb_task.rb +++ b/src/ruby/lib/rgb_task.rb @@ -14,6 +14,8 @@ hue = 0 end +puts "Starting rgb task ..." + while true if $rgb.key? case $rgb.action diff --git a/src/ruby/lib/rotary_encoder.rb b/src/ruby/lib/rotary_encoder.rb index d212d508..6c0d0b17 100644 --- a/src/ruby/lib/rotary_encoder.rb +++ b/src/ruby/lib/rotary_encoder.rb @@ -1,5 +1,6 @@ class RotaryEncoder - def initialize(pin_a, pin_b) + def initialize(pin_a, pin_b) + puts "Initializing RotaryEncoder ..." @pin_a = pin_a @pin_b = pin_b @rotation = 0 diff --git a/src/ruby/lib/tud.rb b/src/ruby/lib/tud.rb index bb9e38cc..4c06522c 100644 --- a/src/ruby/lib/tud.rb +++ b/src/ruby/lib/tud.rb @@ -1,4 +1,13 @@ +print "\033[2J" # clear all +print "\033[1;1H" # home +puts "Welcome to PRK Firmware" +puts +puts "Starting tud task ..." + while true tud_task - $encoders.each { |encoder| encoder.read } unless $encoders.empty? + cdc_task + if $encoders && !$encoders.empty? + $encoders.each { |encoder| encoder.read } + end end diff --git a/src/ruby/sig/object.rbs b/src/ruby/sig/object.rbs index 17b2778a..6571a6dc 100644 --- a/src/ruby/sig/object.rbs +++ b/src/ruby/sig/object.rbs @@ -10,6 +10,7 @@ class Object # Raspi def board_millis : -> Integer + def cdc_task : -> void def tud_task : -> void def tud_mounted? : () -> bool def gpio_init : (Integer) -> void diff --git a/src/usb.c b/src/usb.c index b5fe64d4..160cdfc0 100644 --- a/src/usb.c +++ b/src/usb.c @@ -72,26 +72,26 @@ const uint16_t string_desc_product[] = { // Index: 1 'P', 'R', 'K', 'f', 'i', 'r', 'm' }; -uint8_t const *tud_descriptor_device_cb(void) { - return device_desc; -} -uint8_t const *tud_descriptor_configuration_cb(uint8_t index) { - return conf_desc; -} -uint16_t const *tud_descriptor_string_cb(uint8_t index, uint16_t langid) { - uint16_t const *ret = NULL; - switch(index) { - case 0: - ret = string_desc_lang; - break; - case 1: - ret = string_desc_product; - break; - default: - break; - } - return ret; -} +//uint8_t const *tud_descriptor_device_cb(void) { +// return device_desc; +//} +//uint8_t const *tud_descriptor_configuration_cb(uint8_t index) { +// return conf_desc; +//} +//uint16_t const *tud_descriptor_string_cb(uint8_t index, uint16_t langid) { +// uint16_t const *ret = NULL; +// switch(index) { +// case 0: +// ret = string_desc_lang; +// break; +// case 1: +// ret = string_desc_product; +// break; +// default: +// break; +// } +// return ret; +//} uint8_t const *tud_hid_descriptor_report_cb(uint8_t instance) { diff --git a/src/usb_descriptors.c b/src/usb_descriptors.c new file mode 100644 index 00000000..9702a443 --- /dev/null +++ b/src/usb_descriptors.c @@ -0,0 +1,257 @@ +/* + * 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. + * + */ + +#include "tusb.h" + +/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. + * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. + * + * Auto ProductID layout's Bitmap: + * [MSB] HID | MSC | CDC [LSB] + */ +#define _PID_MAP(itf, n) ( (CFG_TUD_##itf) << (n) ) +#define USB_PID (0x8000 | _PID_MAP(CDC, 0) | _PID_MAP(MSC, 1) | _PID_MAP(HID, 2) | \ + _PID_MAP(MIDI, 3) | _PID_MAP(VENDOR, 4) ) + +//--------------------------------------------------------------------+ +// Device Descriptors +//--------------------------------------------------------------------+ +tusb_desc_device_t const desc_device = +{ + .bLength = sizeof(tusb_desc_device_t), + .bDescriptorType = TUSB_DESC_DEVICE, + .bcdUSB = 0x0200, + + // Use Interface Association Descriptor (IAD) for CDC + // As required by USB Specs IAD's subclass must be common class (2) and protocol must be IAD (1) + .bDeviceClass = TUSB_CLASS_MISC, + .bDeviceSubClass = MISC_SUBCLASS_COMMON, + .bDeviceProtocol = MISC_PROTOCOL_IAD, + + .bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE, + + .idVendor = 0xCafe, + .idProduct = USB_PID, + .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; +} + +//--------------------------------------------------------------------+ +// Configuration Descriptor +//--------------------------------------------------------------------+ + +enum +{ + ITF_NUM_CDC = 0, + ITF_NUM_CDC_DATA, + ITF_NUM_MSC, +// ITF_NUM_HID, + ITF_NUM_TOTAL +}; + +#define CONFIG_TOTAL_LEN (TUD_CONFIG_DESC_LEN + TUD_CDC_DESC_LEN + TUD_MSC_DESC_LEN) +//#define CONFIG_TOTAL_LEN (TUD_CONFIG_DESC_LEN + TUD_CDC_DESC_LEN + TUD_MSC_DESC_LEN + TUD_HID_DESC_LEN) +//#define CONFIG_TOTAL_LEN (TUD_CONFIG_DESC_LEN + TUD_CDC_DESC_LEN + TUD_MSC_DESC_LEN + TUD_HID_INOUT_DESC_LEN) + +#if CFG_TUSB_MCU == OPT_MCU_LPC175X_6X || CFG_TUSB_MCU == OPT_MCU_LPC177X_8X || CFG_TUSB_MCU == OPT_MCU_LPC40XX + // LPC 17xx and 40xx endpoint type (bulk/interrupt/iso) are fixed by its number + // 0 control, 1 In, 2 Bulk, 3 Iso, 4 In, 5 Bulk etc ... + #define EPNUM_CDC_NOTIF 0x81 + #define EPNUM_CDC_OUT 0x02 + #define EPNUM_CDC_IN 0x82 + + #define EPNUM_MSC_OUT 0x05 + #define EPNUM_MSC_IN 0x85 + +#elif CFG_TUSB_MCU == OPT_MCU_SAMG || CFG_TUSB_MCU == OPT_MCU_SAMX7X + // SAMG & SAME70 don't support a same endpoint number with different direction IN and OUT + // e.g EP1 OUT & EP1 IN cannot exist together + #define EPNUM_CDC_NOTIF 0x81 + #define EPNUM_CDC_OUT 0x02 + #define EPNUM_CDC_IN 0x83 + + #define EPNUM_MSC_OUT 0x04 + #define EPNUM_MSC_IN 0x85 + +#elif CFG_TUSB_MCU == OPT_MCU_CXD56 + // CXD56 doesn't support a same endpoint number with different direction IN and OUT + // e.g EP1 OUT & EP1 IN cannot exist together + // CXD56 USB driver has fixed endpoint type (bulk/interrupt/iso) and direction (IN/OUT) by its number + // 0 control (IN/OUT), 1 Bulk (IN), 2 Bulk (OUT), 3 In (IN), 4 Bulk (IN), 5 Bulk (OUT), 6 In (IN) + #define EPNUM_CDC_NOTIF 0x83 + #define EPNUM_CDC_OUT 0x02 + #define EPNUM_CDC_IN 0x81 + + #define EPNUM_MSC_OUT 0x05 + #define EPNUM_MSC_IN 0x84 + +#else + #define EPNUM_CDC_NOTIF 0x81 + #define EPNUM_CDC_OUT 0x02 + #define EPNUM_CDC_IN 0x82 + + #define EPNUM_MSC_OUT 0x03 + #define EPNUM_MSC_IN 0x83 + +#endif + +//enum { +// REPORT_ID_KEYBOARD = 1, +// REPORT_ID_MOUSE +//}; +enum +{ + REPORT_ID_KEYBOARD = 1, + REPORT_ID_MOUSE, + REPORT_ID_CONSUMER_CONTROL, + REPORT_ID_GAMEPAD, + REPORT_ID_COUNT +}; + +#define EPNUM_HID 0x81 + +uint8_t const desc_hid_report[] = +{ + TUD_HID_REPORT_DESC_KEYBOARD( HID_REPORT_ID(REPORT_ID_KEYBOARD )), + TUD_HID_REPORT_DESC_MOUSE ( HID_REPORT_ID(REPORT_ID_MOUSE )), + TUD_HID_REPORT_DESC_CONSUMER( HID_REPORT_ID(REPORT_ID_CONSUMER_CONTROL )), + TUD_HID_REPORT_DESC_GAMEPAD ( HID_REPORT_ID(REPORT_ID_GAMEPAD )) +}; +uint8_t const desc_fs_configuration[] = +{ + // Config number, interface count, string index, total length, attribute, power in mA + TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, CONFIG_TOTAL_LEN, 0x00, 100), + + // Interface number, string index, EP notification address and size, EP data address (out, in) and size. + TUD_CDC_DESCRIPTOR(ITF_NUM_CDC, 4, EPNUM_CDC_NOTIF, 8, EPNUM_CDC_OUT, EPNUM_CDC_IN, 64), + + // Interface number, string index, EP Out & EP In address, EP size + TUD_MSC_DESCRIPTOR(ITF_NUM_MSC, 5, EPNUM_MSC_OUT, EPNUM_MSC_IN, 64), + + // Interface number, string index, protocol, report descriptor len, EP In address, size & polling interval + //TUD_HID_DESCRIPTOR(ITF_NUM_HID, 6, HID_ITF_PROTOCOL_NONE, sizeof(desc_hid_report), EPNUM_HID, CFG_TUD_HID_EP_BUFSIZE, 5) + //TUD_HID_INOUT_DESCRIPTOR(ITF_NUM_HID, 0, HID_ITF_PROTOCOL_NONE, sizeof(desc_hid_report), EPNUM_HID, 0x80 | EPNUM_HID, CFG_TUD_HID_EP_BUFSIZE, 10) +}; + +#if TUD_OPT_HIGH_SPEED +uint8_t const desc_hs_configuration[] = +{ + // Config number, interface count, string index, total length, attribute, power in mA + TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, CONFIG_TOTAL_LEN, 0x00, 100), + + // Interface number, string index, EP notification address and size, EP data address (out, in) and size. + TUD_CDC_DESCRIPTOR(ITF_NUM_CDC, 4, EPNUM_CDC_NOTIF, 8, EPNUM_CDC_OUT, EPNUM_CDC_IN, 512), + + // Interface number, string index, EP Out & EP In address, EP size + TUD_MSC_DESCRIPTOR(ITF_NUM_MSC, 5, EPNUM_MSC_OUT, EPNUM_MSC_IN, 512), + + // Interface number, string index, protocol, report descriptor len, EP In address, size & polling interval + //TUD_HID_DESCRIPTOR(ITF_NUM_HID, 6, HID_ITF_PROTOCOL_NONE, sizeof(desc_hid_report), EPNUM_HID, CFG_TUD_HID_EP_BUFSIZE, 5) + //TUD_HID_INOUT_DESCRIPTOR(ITF_NUM_HID, 0, HID_ITF_PROTOCOL_NONE, sizeof(desc_hid_report), EPNUM_HID, 0x80 | EPNUM_HID, CFG_TUD_HID_EP_BUFSIZE, 10) +}; +#endif + + +// 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 + +#if TUD_OPT_HIGH_SPEED + // Although we are highspeed, host may be fullspeed. + return (tud_speed_get() == TUSB_SPEED_HIGH) ? desc_hs_configuration : desc_fs_configuration; +#else + return desc_fs_configuration; +#endif +} + +//--------------------------------------------------------------------+ +// String Descriptors +//--------------------------------------------------------------------+ + +// array of pointer to string descriptors +char const* string_desc_arr [] = +{ + (const char[]) { 0x09, 0x04 }, // 0: is supported language is English (0x0409) + "Raspberry Pi", // 1: Manufacturer + "RP2040", // 2: Product + "123456", // 3: Serials, should use chip ID + "TinyUSB CDC", // 4: CDC Interface + "TinyUSB MSC", // 5: MSC Interface +}; + +static uint16_t _desc_str[32]; + +// 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 + { + // Note: the 0xEE index string is a Microsoft OS 1.0 Descriptors. + // https://docs.microsoft.com/en-us/windows-hardware/drivers/usbcon/microsoft-defined-usb-descriptors + + 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; + + // Convert ASCII string into UTF-16 + 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; +} diff --git a/src/version.h b/src/version.h new file mode 100644 index 00000000..1f235afb --- /dev/null +++ b/src/version.h @@ -0,0 +1,5 @@ +#pragma once + +#define PRK_VERSION "0.9.0" +#define PRK_BUILDDATE "20210907" +#define PRK_REVISION "e8ee115" diff --git a/src/version.h.in b/src/version.h.in new file mode 100644 index 00000000..5c8c7389 --- /dev/null +++ b/src/version.h.in @@ -0,0 +1,5 @@ +#pragma once + +#define PRK_VERSION "@PRK_VERSION@" +#define PRK_BUILDDATE "@PRK_BUILDDATE@" +#define PRK_REVISION "@PRK_REVISION@" From 3e3bf2b053362b1793675ef4e6f199e041f09bd4 Mon Sep 17 00:00:00 2001 From: HASUMI Hitoshi <hasumikin@gmail.com> Date: Wed, 8 Sep 2021 10:17:42 +0900 Subject: [PATCH 02/10] =?UTF-8?q?MSC=20and=20HID=20work=20together!=20(onl?= =?UTF-8?q?y=20on=20Debug=20=F0=9F=98=95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/msc_disk.c | 2 +- src/usb_descriptors.c | 17 +++++++---------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/msc_disk.c b/src/msc_disk.c index ad7c418b..46c42537 100644 --- a/src/msc_disk.c +++ b/src/msc_disk.c @@ -305,7 +305,7 @@ void msc_init(void) ) #endif { - /* These functions know XIP_BASE */ + /* These functions know XIP_BASE so you just have to give them FLASH_TARGET_OFFSET */ flash_range_erase(FLASH_TARGET_OFFSET, SECTOR_SIZE * 4); flash_range_program(FLASH_TARGET_OFFSET, msc_disk[0], SECTOR_SIZE * 4); } diff --git a/src/usb_descriptors.c b/src/usb_descriptors.c index 9702a443..8c3c0436 100644 --- a/src/usb_descriptors.c +++ b/src/usb_descriptors.c @@ -79,13 +79,11 @@ enum ITF_NUM_CDC = 0, ITF_NUM_CDC_DATA, ITF_NUM_MSC, -// ITF_NUM_HID, + ITF_NUM_HID, ITF_NUM_TOTAL }; -#define CONFIG_TOTAL_LEN (TUD_CONFIG_DESC_LEN + TUD_CDC_DESC_LEN + TUD_MSC_DESC_LEN) -//#define CONFIG_TOTAL_LEN (TUD_CONFIG_DESC_LEN + TUD_CDC_DESC_LEN + TUD_MSC_DESC_LEN + TUD_HID_DESC_LEN) -//#define CONFIG_TOTAL_LEN (TUD_CONFIG_DESC_LEN + TUD_CDC_DESC_LEN + TUD_MSC_DESC_LEN + TUD_HID_INOUT_DESC_LEN) +#define CONFIG_TOTAL_LEN (TUD_CONFIG_DESC_LEN + TUD_CDC_DESC_LEN + TUD_MSC_DESC_LEN + TUD_HID_INOUT_DESC_LEN) #if CFG_TUSB_MCU == OPT_MCU_LPC175X_6X || CFG_TUSB_MCU == OPT_MCU_LPC177X_8X || CFG_TUSB_MCU == OPT_MCU_LPC40XX // LPC 17xx and 40xx endpoint type (bulk/interrupt/iso) are fixed by its number @@ -142,15 +140,15 @@ enum REPORT_ID_COUNT }; -#define EPNUM_HID 0x81 +#define EPNUM_HID_IN 0x04 +#define EPNUM_HID_OUT 0x84 uint8_t const desc_hid_report[] = { TUD_HID_REPORT_DESC_KEYBOARD( HID_REPORT_ID(REPORT_ID_KEYBOARD )), TUD_HID_REPORT_DESC_MOUSE ( HID_REPORT_ID(REPORT_ID_MOUSE )), - TUD_HID_REPORT_DESC_CONSUMER( HID_REPORT_ID(REPORT_ID_CONSUMER_CONTROL )), - TUD_HID_REPORT_DESC_GAMEPAD ( HID_REPORT_ID(REPORT_ID_GAMEPAD )) }; + uint8_t const desc_fs_configuration[] = { // Config number, interface count, string index, total length, attribute, power in mA @@ -163,8 +161,7 @@ uint8_t const desc_fs_configuration[] = TUD_MSC_DESCRIPTOR(ITF_NUM_MSC, 5, EPNUM_MSC_OUT, EPNUM_MSC_IN, 64), // Interface number, string index, protocol, report descriptor len, EP In address, size & polling interval - //TUD_HID_DESCRIPTOR(ITF_NUM_HID, 6, HID_ITF_PROTOCOL_NONE, sizeof(desc_hid_report), EPNUM_HID, CFG_TUD_HID_EP_BUFSIZE, 5) - //TUD_HID_INOUT_DESCRIPTOR(ITF_NUM_HID, 0, HID_ITF_PROTOCOL_NONE, sizeof(desc_hid_report), EPNUM_HID, 0x80 | EPNUM_HID, CFG_TUD_HID_EP_BUFSIZE, 10) + TUD_HID_INOUT_DESCRIPTOR(ITF_NUM_HID, 0, 0, sizeof(desc_hid_report), EPNUM_HID_OUT, EPNUM_HID_IN, 64, 0x08), }; #if TUD_OPT_HIGH_SPEED @@ -180,7 +177,7 @@ uint8_t const desc_hs_configuration[] = TUD_MSC_DESCRIPTOR(ITF_NUM_MSC, 5, EPNUM_MSC_OUT, EPNUM_MSC_IN, 512), // Interface number, string index, protocol, report descriptor len, EP In address, size & polling interval - //TUD_HID_DESCRIPTOR(ITF_NUM_HID, 6, HID_ITF_PROTOCOL_NONE, sizeof(desc_hid_report), EPNUM_HID, CFG_TUD_HID_EP_BUFSIZE, 5) + //TUD_HID_DESCRIPTOR(ITF_NUM_HID, 0, HID_ITF_PROTOCOL_NONE, sizeof(desc_hid_report), EPNUM_HID, CFG_TUD_HID_EP_BUFSIZE, 5) //TUD_HID_INOUT_DESCRIPTOR(ITF_NUM_HID, 0, HID_ITF_PROTOCOL_NONE, sizeof(desc_hid_report), EPNUM_HID, 0x80 | EPNUM_HID, CFG_TUD_HID_EP_BUFSIZE, 10) }; #endif From 00d99a427418a864dcb64e3ee727501dfbfaa6d5 Mon Sep 17 00:00:00 2001 From: HASUMI Hitoshi <hasumikin@gmail.com> Date: Wed, 8 Sep 2021 12:58:28 +0900 Subject: [PATCH 03/10] =?UTF-8?q?MSC=20finally=20works=20=F0=9F=8E=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/msc_disk.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/msc_disk.c b/src/msc_disk.c index 46c42537..63ff5b99 100644 --- a/src/msc_disk.c +++ b/src/msc_disk.c @@ -306,8 +306,10 @@ void msc_init(void) #endif { /* These functions know XIP_BASE so you just have to give them FLASH_TARGET_OFFSET */ + uint32_t ints = save_and_disable_interrupts(); flash_range_erase(FLASH_TARGET_OFFSET, SECTOR_SIZE * 4); flash_range_program(FLASH_TARGET_OFFSET, msc_disk[0], SECTOR_SIZE * 4); + restore_interrupts(ints); } } From ab31af560fa31866dc8b1cc4426423cf1dfde59e Mon Sep 17 00:00:00 2001 From: HASUMI Hitoshi <hasumikin@gmail.com> Date: Wed, 8 Sep 2021 15:05:17 +0900 Subject: [PATCH 04/10] =?UTF-8?q?=F0=9F=92=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CMakeLists.txt | 1 - include/usb_descriptors.h | 34 -------- src/main.c | 4 +- src/usb.c | 143 ------------------------------- src/usb_descriptors.c | 123 ++++++++++++++++---------- src/{usb.h => usb_descriptors.h} | 16 +++- src/version.h | 2 +- 7 files changed, 95 insertions(+), 228 deletions(-) delete mode 100644 include/usb_descriptors.h delete mode 100644 src/usb.c rename src/{usb.h => usb_descriptors.h} (69%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 981e045b..8c968222 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -31,7 +31,6 @@ add_executable(${PROJECT} src/msc_disk.c src/usb_descriptors.c src/gpio.c - src/usb.c src/uart.c src/ws2812.c src/rotary_encoder.c diff --git a/include/usb_descriptors.h b/include/usb_descriptors.h deleted file mode 100644 index 1359d1f0..00000000 --- a/include/usb_descriptors.h +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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 USB_DESCRIPTORS_H_ -#define USB_DESCRIPTORS_H_ - -enum { - REPORT_ID_KEYBOARD = 1, - REPORT_ID_MOUSE -}; - -#endif /* USB_DESCRIPTORS_H_ */ - diff --git a/src/main.c b/src/main.c index 84c7f707..cb0d25f5 100644 --- a/src/main.c +++ b/src/main.c @@ -11,7 +11,7 @@ /* mrbc_class */ #include "msc_disk.h" #include "gpio.h" -#include "usb.h" +#include "usb_descriptors.h" #include "uart.h" #include "ws2812.h" #include "rotary_encoder.h" @@ -80,7 +80,7 @@ int main() { mrbc_define_method(0, mrbc_class_object, "rand", c_rand); MSC_INIT(); GPIO_INIT(); - USB_INIT(); + TUD_INIT(); UART_INIT(); WS2812_INIT(); ROTARY_ENCODER_INIT(); diff --git a/src/usb.c b/src/usb.c deleted file mode 100644 index 160cdfc0..00000000 --- a/src/usb.c +++ /dev/null @@ -1,143 +0,0 @@ -#include "usb.h" - -#include "tusb.h" -#include "usb_descriptors.h" -#include "hardware/timer.h" -#include "hardware/clocks.h" -#include "hardware/irq.h" -#include "hardware/structs/scb.h" -#include "hardware/sync.h" -#include "hardware/gpio.h" - -const uint8_t device_desc[] = { - 18, // bLength - 1, // bDescriptorType - 0x10, - 0x01, // bcdUSB - 0x00, // bDeviceClass - 0x00, // bDeviceSubClass - 0x00, // bDeviceProtocol - CFG_TUD_ENDPOINT0_SIZE, // bMaxPacketSize0 - 0xfe, - 0xca, // idVendor - 0xcd, - 0xab, // idProduct, - 0x00, - 0x00, // bcdDevice - 0x00, // iManufacturer - 0x01, // iProduct - 0x00, // iSerialNumber - 0x01, // bNumConfigurations -}; - -//const uint8_t conf_desc[] = { -// 9, // bLength -// 2, // bDescriptorType -// 9 + 9, -// 0, // wTotalLength -// 1, // bNumInterface -// 1, // bConfigurationValue -// 0, // iConfiguration -// 0x20, // bmAttributes -// 0x0F, // bMaxPower -// -// // --- Interface --- -// 9, // bLength -// 4, // bDescriptorType -// 1, // bInterfaceNumber -// 0, // bAlternateSetting -// 0, // bNumEndpoints -// 0xFF, // bInterfaceClass -// 0xFF, // bInterfaceSubClass -// 0xFF, // bInterfaceProtocol -// 0, // iInterface -//}; - -const uint8_t hid_report_desc[] = { - TUD_HID_REPORT_DESC_KEYBOARD(HID_REPORT_ID(REPORT_ID_KEYBOARD)), - TUD_HID_REPORT_DESC_MOUSE(HID_REPORT_ID(REPORT_ID_MOUSE)) -}; - -const uint8_t conf_desc[] = { - TUD_CONFIG_DESCRIPTOR(1, 1, 0, TUD_CONFIG_DESC_LEN + TUD_HID_INOUT_DESC_LEN, 0, 0x0F), - TUD_HID_INOUT_DESCRIPTOR(1, 0, 0, sizeof(hid_report_desc), 0x01, 0x81, 64, 0x0F) -}; - -const uint16_t string_desc_lang[] = { // Index: 0 - 4 | (3 << 8), // bLength & bDescriptorType - 0 -}; -const uint16_t string_desc_product[] = { // Index: 1 - 16 | (3 << 8), - 'P', 'R', 'K', 'f', 'i', 'r', 'm' -}; - -//uint8_t const *tud_descriptor_device_cb(void) { -// return device_desc; -//} -//uint8_t const *tud_descriptor_configuration_cb(uint8_t index) { -// return conf_desc; -//} -//uint16_t const *tud_descriptor_string_cb(uint8_t index, uint16_t langid) { -// uint16_t const *ret = NULL; -// switch(index) { -// case 0: -// ret = string_desc_lang; -// break; -// case 1: -// ret = string_desc_product; -// break; -// default: -// break; -// } -// return ret; -//} - - -uint8_t const *tud_hid_descriptor_report_cb(uint8_t instance) { - return hid_report_desc; -} -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) { - return 0; -} -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) { - return; -} - - -void -c_tud_task(mrb_vm *vm, mrb_value *v, int argc) -{ - tud_task(); -} - -void -c_report_hid(mrb_vm *vm, mrb_value *v, int argc) -{ - uint32_t const btn = 1; - - // 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(); - } - - uint8_t modifier = GET_INT_ARG(1); - uint8_t *keycodes = GET_STRING_ARG(2); - - /*------------- Keyboard -------------*/ - if (tud_hid_ready()) { - tud_hid_keyboard_report(REPORT_ID_KEYBOARD, modifier, keycodes); - } -} - -void -c_tud_mounted_q(mrb_vm *vm, mrb_value *v, int argc) -{ - if (tud_mounted()) { - SET_TRUE_RETURN(); - } else { - SET_FALSE_RETURN(); - } -} diff --git a/src/usb_descriptors.c b/src/usb_descriptors.c index 8c3c0436..b4abc848 100644 --- a/src/usb_descriptors.c +++ b/src/usb_descriptors.c @@ -25,6 +25,8 @@ #include "tusb.h" +#include "usb_descriptors.h" + /* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. * @@ -74,15 +76,6 @@ uint8_t const * tud_descriptor_device_cb(void) // Configuration Descriptor //--------------------------------------------------------------------+ -enum -{ - ITF_NUM_CDC = 0, - ITF_NUM_CDC_DATA, - ITF_NUM_MSC, - ITF_NUM_HID, - ITF_NUM_TOTAL -}; - #define CONFIG_TOTAL_LEN (TUD_CONFIG_DESC_LEN + TUD_CDC_DESC_LEN + TUD_MSC_DESC_LEN + TUD_HID_INOUT_DESC_LEN) #if CFG_TUSB_MCU == OPT_MCU_LPC175X_6X || CFG_TUSB_MCU == OPT_MCU_LPC177X_8X || CFG_TUSB_MCU == OPT_MCU_LPC40XX @@ -127,19 +120,6 @@ enum #endif -//enum { -// REPORT_ID_KEYBOARD = 1, -// REPORT_ID_MOUSE -//}; -enum -{ - REPORT_ID_KEYBOARD = 1, - REPORT_ID_MOUSE, - REPORT_ID_CONSUMER_CONTROL, - REPORT_ID_GAMEPAD, - REPORT_ID_COUNT -}; - #define EPNUM_HID_IN 0x04 #define EPNUM_HID_OUT 0x84 @@ -149,6 +129,14 @@ uint8_t const desc_hid_report[] = TUD_HID_REPORT_DESC_MOUSE ( HID_REPORT_ID(REPORT_ID_MOUSE )), }; +enum +{ + ITF_NUM_CDC = 0, + ITF_NUM_HID, + ITF_NUM_MSC, + ITF_NUM_TOTAL +}; + uint8_t const desc_fs_configuration[] = { // Config number, interface count, string index, total length, attribute, power in mA @@ -164,24 +152,6 @@ uint8_t const desc_fs_configuration[] = TUD_HID_INOUT_DESCRIPTOR(ITF_NUM_HID, 0, 0, sizeof(desc_hid_report), EPNUM_HID_OUT, EPNUM_HID_IN, 64, 0x08), }; -#if TUD_OPT_HIGH_SPEED -uint8_t const desc_hs_configuration[] = -{ - // Config number, interface count, string index, total length, attribute, power in mA - TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, CONFIG_TOTAL_LEN, 0x00, 100), - - // Interface number, string index, EP notification address and size, EP data address (out, in) and size. - TUD_CDC_DESCRIPTOR(ITF_NUM_CDC, 4, EPNUM_CDC_NOTIF, 8, EPNUM_CDC_OUT, EPNUM_CDC_IN, 512), - - // Interface number, string index, EP Out & EP In address, EP size - TUD_MSC_DESCRIPTOR(ITF_NUM_MSC, 5, EPNUM_MSC_OUT, EPNUM_MSC_IN, 512), - - // Interface number, string index, protocol, report descriptor len, EP In address, size & polling interval - //TUD_HID_DESCRIPTOR(ITF_NUM_HID, 0, HID_ITF_PROTOCOL_NONE, sizeof(desc_hid_report), EPNUM_HID, CFG_TUD_HID_EP_BUFSIZE, 5) - //TUD_HID_INOUT_DESCRIPTOR(ITF_NUM_HID, 0, HID_ITF_PROTOCOL_NONE, sizeof(desc_hid_report), EPNUM_HID, 0x80 | EPNUM_HID, CFG_TUD_HID_EP_BUFSIZE, 10) -}; -#endif - // Invoked when received GET CONFIGURATION DESCRIPTOR // Application return pointer to descriptor @@ -189,13 +159,7 @@ uint8_t const desc_hs_configuration[] = uint8_t const * tud_descriptor_configuration_cb(uint8_t index) { (void) index; // for multiple configurations - -#if TUD_OPT_HIGH_SPEED - // Although we are highspeed, host may be fullspeed. - return (tud_speed_get() == TUSB_SPEED_HIGH) ? desc_hs_configuration : desc_fs_configuration; -#else return desc_fs_configuration; -#endif } //--------------------------------------------------------------------+ @@ -252,3 +216,70 @@ uint16_t const* tud_descriptor_string_cb(uint8_t index, uint16_t langid) return _desc_str; } + + +//--------------------------------------------------------------------+ +// HID Descriptor +//--------------------------------------------------------------------+ + +const uint8_t hid_report_desc[] = { + TUD_HID_REPORT_DESC_KEYBOARD(HID_REPORT_ID(REPORT_ID_KEYBOARD)), + TUD_HID_REPORT_DESC_MOUSE(HID_REPORT_ID(REPORT_ID_MOUSE)) +}; + +const uint16_t string_desc_product[] = { // Index: 1 + 16 | (3 << 8), + 'P', 'R', 'K', 'f', 'i', 'r', 'm' +}; + +uint8_t const *tud_hid_descriptor_report_cb(uint8_t instance) { + return hid_report_desc; +} +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) { + return 0; +} +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) { + return; +} + + +//--------------------------------------------------------------------+ +// Ruby methods +//--------------------------------------------------------------------+ + +void +c_tud_task(mrb_vm *vm, mrb_value *v, int argc) +{ + tud_task(); +} + +void +c_report_hid(mrb_vm *vm, mrb_value *v, int argc) +{ + uint32_t const btn = 1; + + // 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(); + } + + uint8_t modifier = GET_INT_ARG(1); + uint8_t *keycodes = GET_STRING_ARG(2); + + /*------------- Keyboard -------------*/ + if (tud_hid_ready()) { + tud_hid_keyboard_report(REPORT_ID_KEYBOARD, modifier, keycodes); + } +} + +void +c_tud_mounted_q(mrb_vm *vm, mrb_value *v, int argc) +{ + if (tud_mounted()) { + SET_TRUE_RETURN(); + } else { + SET_FALSE_RETURN(); + } +} diff --git a/src/usb.h b/src/usb_descriptors.h similarity index 69% rename from src/usb.h rename to src/usb_descriptors.h index a992abf7..184baf2d 100644 --- a/src/usb.h +++ b/src/usb_descriptors.h @@ -1,14 +1,28 @@ /* mruby/c VM */ #include <mrubyc.h> +#ifndef USB_DESCRIPTORS_H_ +#define USB_DESCRIPTORS_H_ + +enum { + REPORT_ID_KEYBOARD = 1, + REPORT_ID_MOUSE, + REPORT_ID_CONSUMER_CONTROL, + REPORT_ID_GAMEPAD, + REPORT_ID_COUNT +}; + void c_tud_task(mrb_vm *vm, mrb_value *v, int argc); void c_report_hid(mrb_vm *vm, mrb_value *v, int argc); void c_tud_mounted_q(mrb_vm *vm, mrb_value *v, int argc); -#define USB_INIT() do { \ +#define TUD_INIT() do { \ mrbc_define_method(0, mrbc_class_object, "tud_task", c_tud_task); \ mrbc_define_method(0, mrbc_class_object, "tud_mounted?", c_tud_mounted_q); \ mrbc_class *mrbc_class_Keyboard = mrbc_define_class(0, "Keyboard", mrbc_class_object); \ mrbc_define_method(0, mrbc_class_Keyboard, "report_hid", c_report_hid); \ } while (0) +#endif /* USB_DESCRIPTORS_H_ */ + + diff --git a/src/version.h b/src/version.h index 1f235afb..4652a730 100644 --- a/src/version.h +++ b/src/version.h @@ -1,5 +1,5 @@ #pragma once #define PRK_VERSION "0.9.0" -#define PRK_BUILDDATE "20210907" +#define PRK_BUILDDATE "20210908" #define PRK_REVISION "e8ee115" From 31da07781930e1f3a99cc404c829a47ed3cead32 Mon Sep 17 00:00:00 2001 From: HASUMI Hitoshi <hasumikin@gmail.com> Date: Wed, 8 Sep 2021 16:59:49 +0900 Subject: [PATCH 05/10] wip --- src/main.c | 55 +++++++++++++++++++++++++++++++++++++++++ src/msc_disk.c | 31 +---------------------- src/msc_disk.h | 35 ++++++++++++++++++++++++++ src/ruby/lib/tud.rb | 13 ++++++++++ src/ruby/sig/object.rbs | 2 ++ 5 files changed, 106 insertions(+), 30 deletions(-) diff --git a/src/main.c b/src/main.c index cb0d25f5..b7bf1cf4 100644 --- a/src/main.c +++ b/src/main.c @@ -68,9 +68,60 @@ mrbc_load_model(const uint8_t *mrb) mrbc_raw_free(vm); } +#ifndef NODE_BOX_SIZE +#define NODE_BOX_SIZE 30 +#endif + +//mrbc_tcb* +//autoreload(void) +//{ +// if (autoreload_state != AUTORELOAD_READY) return NULL; +// autoreload_state = AUTORELOAD_WAIT; +// DirEnt entry; +// msc_findDirEnt("KEYMAP RB ", &entry); +// if (entry.Name[0] != '\0') { +// char *program = (char *)(FLASH_MMAP_ADDR + SECTOR_SIZE * (1 + entry.FstClusLO)); +// ParserState *p = Compiler_parseInitState(NODE_BOX_SIZE); +// StreamInterface *si = StreamInterface_new(program, STREAM_TYPE_MEMORY); +// mrbc_tcb *tcb; +// if (Compiler_compile(p, si)) { +// tcb = mrbc_create_task(p->scope->vm_code, 0); +// p->scope->vm_code = NULL; +// Compiler_parserStateFree(p); +// } +// StreamInterface_free(si); +// return tcb; +// } else { +// return NULL; +// } +//} + + + +mrbc_tcb *tcb_keymap; + mrbc_tcb *tcb_rgb; /* from ws2812.h */ +//int autoreload_state; /* from msc_disk.h */ + +//void +//c_autoreload_bang(mrb_vm *vm, mrb_value *v, int argc) +//{ +//} +// +//void +//c_autoreload_ready_q(mrb_vm *vm, mrb_value *v, int argc) +//{ +// if (autoreload_state == AUTORELOAD_READY) { +// SET_TRUE_RETURN(); +// } else { +// SET_FALSE_RETURN(); +// } +//} + int main() { +// autoreload_state = AUTORELOAD_READY; + stdio_init_all(); board_init(); tusb_init(); @@ -78,6 +129,8 @@ int main() { mrbc_init(memory_pool, MEMORY_SIZE); mrbc_define_method(0, mrbc_class_object, "board_millis", c_board_millis); mrbc_define_method(0, mrbc_class_object, "rand", c_rand); +// mrbc_define_method(0, mrbc_class_object, "autoreload_ready?", c_autoreload_ready_q); +// mrbc_define_method(0, mrbc_class_object, "autoreload!", c_autoreload_bang); MSC_INIT(); GPIO_INIT(); TUD_INIT(); @@ -93,6 +146,8 @@ int main() { mrbc_create_task(tud, 0); tcb_rgb = mrbc_create_task(rgb_task, 0); create_sandbox(); + //tcb_keymap = autoreload(); +//autoreload_state = AUTORELOAD_WAIT; mrbc_create_task(keymap, 0); mrbc_run(); return 0; diff --git a/src/msc_disk.c b/src/msc_disk.c index 63ff5b99..23ac5be9 100644 --- a/src/msc_disk.c +++ b/src/msc_disk.c @@ -33,11 +33,6 @@ #include "msc_disk.h" -#define FLASH_TARGET_OFFSET 0x00180000 /* 1.5MB */ -#define FLASH_MMAP_ADDR (XIP_BASE + FLASH_TARGET_OFFSET) /* 0x10180000 */ -#define SECTOR_SIZE 4096 -#define SECTOR_COUNT 128 /* 4096 * 128 = 512KB */ - // whether host does safe-eject static bool ejected = false; @@ -313,25 +308,6 @@ void msc_init(void) } } -/* - * Directory entry in FAT Volume - * (FAT data is little endian) - */ -typedef struct dir_ent { - char Name[11]; - uint8_t Attr; - uint8_t NTRes; - uint8_t CrtTimeTenth; - char CrtTime[2]; - char CrtDate[2]; - char LastAccDate[2]; - uint16_t FstClusHI; - char WrtTime[2]; - char WrtDate[2]; - uint16_t FstClusLO; - uint32_t FileSize; -} DirEnt; - /* * Only works in top directory * Only works if SFN (short file name) @@ -359,12 +335,7 @@ void tud_msc_write10_complete_cb(uint8_t lun) { (void)lun; -// DirEnt entry; -// msc_findDirEnt("README TXT", &entry); -// if (entry.Name[0] != '\0') { -// const char *program = (const char *)(FLASH_MMAP_ADDR + SECTOR_SIZE * (1 + entry.FstClusLO)); -// ppp = program[0]; -// } +// autoreload_state = AUTORELOAD_READY; } //--------------------------------------------------------------------+ diff --git a/src/msc_disk.h b/src/msc_disk.h index 8e810e69..8048c58f 100644 --- a/src/msc_disk.h +++ b/src/msc_disk.h @@ -1,7 +1,42 @@ /* mruby/c VM */ #include <mrubyc.h> +#define FLASH_TARGET_OFFSET 0x00180000 /* 1.5MB */ +#define FLASH_MMAP_ADDR (XIP_BASE + FLASH_TARGET_OFFSET) /* 0x10180000 */ +#define SECTOR_SIZE 4096 +#define SECTOR_COUNT 128 /* 4096 * 128 = 512KB */ + +/* + * Directory entry in FAT Volume + * Note that FAT data is little endian + */ +typedef struct dir_ent { + char Name[11]; + uint8_t Attr; + uint8_t NTRes; + uint8_t CrtTimeTenth; + char CrtTime[2]; + char CrtDate[2]; + char LastAccDate[2]; + uint16_t FstClusHI; + char WrtTime[2]; + char WrtDate[2]; + uint16_t FstClusLO; + uint32_t FileSize; +} DirEnt; + +enum +{ + AUTORELOAD_WAIT = 0, + AUTORELOAD_READY +}; + +extern int autoreload_state; + void msc_init(void); + +void msc_findDirEnt(const char *filename, DirEnt *entry); + void c_cdc_task(mrb_vm *vm, mrb_value *v, int argc); #define MSC_INIT() do { \ diff --git a/src/ruby/lib/tud.rb b/src/ruby/lib/tud.rb index 4c06522c..e7060392 100644 --- a/src/ruby/lib/tud.rb +++ b/src/ruby/lib/tud.rb @@ -4,10 +4,23 @@ puts puts "Starting tud task ..." +autoreload_tick = 0 + while true tud_task cdc_task if $encoders && !$encoders.empty? $encoders.each { |encoder| encoder.read } end +# if autoreload_ready? +# if autoreload_tick == 0 +# puts "Autoreload is ready ..." +# autoreload_tick = 500 +# elsif autoreload_tick == 1 +# puts "Autoreload!" +# autoreload! +# autoreload_tick = 0 +# end +# autoreload_tick -= 1 +# end end diff --git a/src/ruby/sig/object.rbs b/src/ruby/sig/object.rbs index 6571a6dc..d2f3bcec 100644 --- a/src/ruby/sig/object.rbs +++ b/src/ruby/sig/object.rbs @@ -7,6 +7,8 @@ class Object def sandbox_result : () -> untyped def compile_ruby: (String) -> bool def invoke_ruby: () -> bool + def autoreload_ready?: () -> bool + def autoreload!: () -> void # Raspi def board_millis : -> Integer From af4b820ae2e002e70fe85137218f7495e189e622 Mon Sep 17 00:00:00 2001 From: HASUMI Hitoshi <hasumikin@gmail.com> Date: Wed, 8 Sep 2021 18:05:34 +0900 Subject: [PATCH 06/10] =?UTF-8?q?Fix=20bug=20=F0=9F=90=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/usb_descriptors.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/usb_descriptors.c b/src/usb_descriptors.c index b4abc848..b98a51b1 100644 --- a/src/usb_descriptors.c +++ b/src/usb_descriptors.c @@ -132,6 +132,7 @@ uint8_t const desc_hid_report[] = enum { ITF_NUM_CDC = 0, + ITF_NUM_CDC_DATA, ITF_NUM_HID, ITF_NUM_MSC, ITF_NUM_TOTAL From 71e1fbd451095c41ca62067b6abb959160c41091 Mon Sep 17 00:00:00 2001 From: HASUMI Hitoshi <hasumikin@gmail.com> Date: Wed, 8 Sep 2021 18:05:54 +0900 Subject: [PATCH 07/10] autoreload on (only) startup!!!!!!! --- CMakeLists.txt | 9 +++++++- src/main.c | 58 +++++++++++++++++++++++++++----------------------- 2 files changed, 39 insertions(+), 28 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8c968222..42c9762b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,14 @@ include(pico_sdk_import.cmake) project(prk_firmware) -add_definitions(-DMRBC_USE_HAL_RP2040 -DMRBC_REQUIRE_32BIT_ALIGNMENT -DMAX_REGS_SIZE=256 -DMAX_SYMBOLS_COUNT=500 -DMAX_VM_COUNT=10) +add_definitions( + -DNDEBUG + -DMRBC_USE_HAL_RP2040 + -DMRBC_REQUIRE_32BIT_ALIGNMENT + -DMAX_REGS_SIZE=256 + -DMAX_SYMBOLS_COUNT=500 + -DMAX_VM_COUNT=10 +) # initialize the Raspberry Pi Pico SDK pico_sdk_init() diff --git a/src/main.c b/src/main.c index b7bf1cf4..68dd93a7 100644 --- a/src/main.c +++ b/src/main.c @@ -72,29 +72,29 @@ mrbc_load_model(const uint8_t *mrb) #define NODE_BOX_SIZE 30 #endif -//mrbc_tcb* -//autoreload(void) -//{ -// if (autoreload_state != AUTORELOAD_READY) return NULL; -// autoreload_state = AUTORELOAD_WAIT; -// DirEnt entry; -// msc_findDirEnt("KEYMAP RB ", &entry); -// if (entry.Name[0] != '\0') { -// char *program = (char *)(FLASH_MMAP_ADDR + SECTOR_SIZE * (1 + entry.FstClusLO)); -// ParserState *p = Compiler_parseInitState(NODE_BOX_SIZE); -// StreamInterface *si = StreamInterface_new(program, STREAM_TYPE_MEMORY); -// mrbc_tcb *tcb; -// if (Compiler_compile(p, si)) { -// tcb = mrbc_create_task(p->scope->vm_code, 0); -// p->scope->vm_code = NULL; -// Compiler_parserStateFree(p); -// } -// StreamInterface_free(si); -// return tcb; -// } else { -// return NULL; -// } -//} +mrbc_tcb* +autoreload(void) +{ + if (autoreload_state != AUTORELOAD_READY) return NULL; + autoreload_state = AUTORELOAD_WAIT; + DirEnt entry; + msc_findDirEnt("KEYMAP RB ", &entry); + if (entry.Name[0] != '\0') { + char *program = (char *)(FLASH_MMAP_ADDR + SECTOR_SIZE * (1 + entry.FstClusLO)); + ParserState *p = Compiler_parseInitState(NODE_BOX_SIZE); + StreamInterface *si = StreamInterface_new(program, STREAM_TYPE_MEMORY); + mrbc_tcb *tcb; + if (Compiler_compile(p, si)) { + tcb = mrbc_create_task(p->scope->vm_code, 0); + p->scope->vm_code = NULL; + Compiler_parserStateFree(p); + } + StreamInterface_free(si); + return tcb; + } else { + return NULL; + } +} @@ -102,7 +102,7 @@ mrbc_tcb *tcb_keymap; mrbc_tcb *tcb_rgb; /* from ws2812.h */ -//int autoreload_state; /* from msc_disk.h */ +int autoreload_state; /* from msc_disk.h */ //void //c_autoreload_bang(mrb_vm *vm, mrb_value *v, int argc) @@ -119,8 +119,12 @@ mrbc_tcb *tcb_rgb; /* from ws2812.h */ // } //} +int loglevel; + int main() { -// autoreload_state = AUTORELOAD_READY; + loglevel = LOGLEVEL_WARN; + + autoreload_state = AUTORELOAD_READY; stdio_init_all(); board_init(); @@ -146,9 +150,9 @@ int main() { mrbc_create_task(tud, 0); tcb_rgb = mrbc_create_task(rgb_task, 0); create_sandbox(); - //tcb_keymap = autoreload(); + tcb_keymap = autoreload(); //autoreload_state = AUTORELOAD_WAIT; - mrbc_create_task(keymap, 0); +// mrbc_create_task(keymap, 0); mrbc_run(); return 0; } From 6e77670b1b8677f16bf27a1b8d7f9682822381a0 Mon Sep 17 00:00:00 2001 From: HASUMI Hitoshi <hasumikin@gmail.com> Date: Thu, 9 Sep 2021 19:52:14 +0900 Subject: [PATCH 08/10] Autoreload works!!!!! --- CHANGELOG.md | 6 +++ README.md | 39 ++++++++++++-- doc/images/drag_and_drop_1.png | Bin 0 -> 27126 bytes doc/images/drag_and_drop_2.png | Bin 0 -> 28235 bytes src/main.c | 94 +++++++++++++++++++++------------ src/msc_disk.c | 2 +- src/ruby/lib/tud.rb | 25 +++++---- src/ruby/sig/object.rbs | 3 +- 8 files changed, 116 insertions(+), 53 deletions(-) create mode 100644 doc/images/drag_and_drop_1.png create mode 100644 doc/images/drag_and_drop_2.png diff --git a/CHANGELOG.md b/CHANGELOG.md index e18a3385..c0573358 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Change log +## 2021/09/10 +### BIG BIG IMPROVEMENT 🍣 +- You no longer need any compiler toolchain!!! +- No longer detaching USB cable every time amending your keymap, too!!! +- See README.md 👀 + ## 2021/08/17 ### Breaking Change 💣 - Code upgraded to correspond to the newest pico-sdk as of today. Please upgrade your pico-sdk diff --git a/README.md b/README.md index a5629443..3d0d0095 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,32 @@ _(left: Raspberry Pi Pico / right: Sparkfun Pro Micro RP2040)_ ### Getting started +There are two ways to install PRK Fiwmware: + +1. [Using a release binary (recommended)](#Using-a-release-binary) +2. [Building a binary by yourself](Building-a-binary-by-yourself) + +Anyhow, you should: + +- Be knowledgeable how to install a UF2 file into Raspi Pico on [https://www.raspberrypi.org/documentation/rp2040/getting-started/#getting-started-with-c](https://www.raspberrypi.org/documentation/rp2040/getting-started/#getting-started-with-c) + - [https://learn.sparkfun.com/tutorials/pro-micro-rp2040-hookup-guide](https://learn.sparkfun.com/tutorials/pro-micro-rp2040-hookup-guide) will also help you if you use Sparkfun Pro Micro RP2040 + +#### Using a release binary + +- Download the newest release binary from [Releases](https://github.com/picoruby/prk_firmware/releases) + +- Unzip it. You should get a file that looks like `prk_firmware-0.9.0-20210910-xxxxxxxx.uf2` + +- Flash the uf2 into RP2040 + +  + +- + +#### Building a binary by yourself + +You may not want PRK Firmware to be a mass storage device in case that your employer doesn't allow you to bring a USB memory 🙈 + - Install CRuby (MRI) because "Static type checking" by [Steep](https://github.com/soutaro/steep) will be invoked in build process - Setup Raspberry Pi Pico C/C++ SDK @@ -45,9 +71,6 @@ _(left: Raspberry Pi Pico / right: Sparkfun Pro Micro RP2040)_ - Follow the instructions on [https://github.com/raspberrypi/pico-sdk#quick-start-your-own-project](https://github.com/raspberrypi/pico-sdk#quick-start-your-own-project) - Make sure you have `PICO_SDK_PATH` environment variable - - Be knowledgeable how to install a UF2 file into Raspi Pico on [https://www.raspberrypi.org/documentation/rp2040/getting-started/#getting-started-with-c](https://www.raspberrypi.org/documentation/rp2040/getting-started/#getting-started-with-c) - - - [https://learn.sparkfun.com/tutorials/pro-micro-rp2040-hookup-guide](https://learn.sparkfun.com/tutorials/pro-micro-rp2040-hookup-guide) will also help you if you use Sparkfun Pro Micro RP2040 - Clone the `prk_firmware` (this repository) wherever you like @@ -63,15 +86,21 @@ _(left: Raspberry Pi Pico / right: Sparkfun Pro Micro RP2040)_ git clone https://github.com/picoruby/prk_meishi2.git ``` +- Edit `prk_meishi2/keymap.rb` as you wish + - Build with `cmake` and `make` ``` cd prk_meishi2/build - cmake ../../.. + cmake -DPRK_NO_MSC=1 ../../.. make ``` + + (Defining PRK_NO_MSC macro will avoid implementing mass storage feature) + + Now you should have `prk_firmware-[version]-[date]-[revision].uf2` file in `prk_firmware/keyboards/prk_meishi2/build/` directory which includes your keymap in code. - Now you should have `prk_firmware.uf2` file in `prk_firmware/keyboards/prk_meishi2/build/` directory. +- Install .uf2 into RP2040 ### Contributing diff --git a/doc/images/drag_and_drop_1.png b/doc/images/drag_and_drop_1.png new file mode 100644 index 0000000000000000000000000000000000000000..a8af438b92d9ca130998ded23728be9e90b0103e GIT binary patch literal 27126 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 X#|S6uPb{vZI|$y%tI8G0eDwcsGIlyp literal 0 HcmV?d00001 diff --git a/doc/images/drag_and_drop_2.png b/doc/images/drag_and_drop_2.png new file mode 100644 index 0000000000000000000000000000000000000000..07572fee91b7e0bf78ce88d1581e3cb60b4c823f GIT binary patch literal 28235 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%0D|?j(rrKR7JgismUN&>o_8cYbtE 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 literal 0 HcmV?d00001 diff --git a/src/main.c b/src/main.c index 68dd93a7..99d883b8 100644 --- a/src/main.c +++ b/src/main.c @@ -69,31 +69,49 @@ mrbc_load_model(const uint8_t *mrb) } #ifndef NODE_BOX_SIZE -#define NODE_BOX_SIZE 30 +#define NODE_BOX_SIZE 50 #endif -mrbc_tcb* -autoreload(void) +mrbc_tcb * +create_keymap_task(mrbc_tcb *tcb) { - if (autoreload_state != AUTORELOAD_READY) return NULL; - autoreload_state = AUTORELOAD_WAIT; + hal_disable_irq(); DirEnt entry; + char *program; + StreamInterface *si; + ParserState *p = Compiler_parseInitState(NODE_BOX_SIZE); msc_findDirEnt("KEYMAP RB ", &entry); if (entry.Name[0] != '\0') { - char *program = (char *)(FLASH_MMAP_ADDR + SECTOR_SIZE * (1 + entry.FstClusLO)); - ParserState *p = Compiler_parseInitState(NODE_BOX_SIZE); - StreamInterface *si = StreamInterface_new(program, STREAM_TYPE_MEMORY); - mrbc_tcb *tcb; - if (Compiler_compile(p, si)) { - tcb = mrbc_create_task(p->scope->vm_code, 0); - p->scope->vm_code = NULL; - Compiler_parserStateFree(p); - } + program = (char *)(FLASH_MMAP_ADDR + SECTOR_SIZE * (1 + entry.FstClusLO)); + si = StreamInterface_new(program, STREAM_TYPE_MEMORY); + } else { + si = StreamInterface_new("suspend_task", STREAM_TYPE_MEMORY); + } + if (!Compiler_compile(p, si)) { + Compiler_parserStateFree(p); StreamInterface_free(si); - return tcb; + p = Compiler_parseInitState(NODE_BOX_SIZE); + si = StreamInterface_new("suspend_task", STREAM_TYPE_MEMORY); + Compiler_compile(p, si); + } + if (tcb == NULL) { + tcb = mrbc_create_task(p->scope->vm_code, 0); } else { - return NULL; + mrbc_vm *vm = (mrbc_vm *)(&tcb->vm); + int vm_id = vm->vm_id; + mrbc_vm_end(vm); + memset(vm, 0, sizeof(mrbc_vm)); + mrbc_load_mrb(vm, p->scope->vm_code); + vm->vm_id = vm_id; + mrbc_vm_begin(vm); } + p->scope->vm_code = NULL; + Compiler_parserStateFree(p); + StreamInterface_free(si); + autoreload_state = AUTORELOAD_WAIT; + hal_enable_irq(); + mrbc_resume_task(tcb); + return tcb; } @@ -104,20 +122,27 @@ mrbc_tcb *tcb_rgb; /* from ws2812.h */ int autoreload_state; /* from msc_disk.h */ -//void -//c_autoreload_bang(mrb_vm *vm, mrb_value *v, int argc) -//{ -//} -// -//void -//c_autoreload_ready_q(mrb_vm *vm, mrb_value *v, int argc) -//{ -// if (autoreload_state == AUTORELOAD_READY) { -// SET_TRUE_RETURN(); -// } else { -// SET_FALSE_RETURN(); -// } -//} +void +c_suspend_keymap(mrb_vm *vm, mrb_value *v, int argc) +{ + mrbc_suspend_task(tcb_keymap); +} + +void +c_reload_keymap(mrb_vm *vm, mrb_value *v, int argc) +{ + tcb_keymap = create_keymap_task(tcb_keymap); +} + +void +c_autoreload_ready_q(mrb_vm *vm, mrb_value *v, int argc) +{ + if (autoreload_state == AUTORELOAD_READY) { + SET_TRUE_RETURN(); + } else { + SET_FALSE_RETURN(); + } +} int loglevel; @@ -133,8 +158,9 @@ int main() { mrbc_init(memory_pool, MEMORY_SIZE); mrbc_define_method(0, mrbc_class_object, "board_millis", c_board_millis); mrbc_define_method(0, mrbc_class_object, "rand", c_rand); -// mrbc_define_method(0, mrbc_class_object, "autoreload_ready?", c_autoreload_ready_q); -// mrbc_define_method(0, mrbc_class_object, "autoreload!", c_autoreload_bang); + mrbc_define_method(0, mrbc_class_object, "autoreload_ready?", c_autoreload_ready_q); + mrbc_define_method(0, mrbc_class_object, "reload_keymap", c_reload_keymap); + mrbc_define_method(0, mrbc_class_object, "suspend_keymap", c_suspend_keymap); MSC_INIT(); GPIO_INIT(); TUD_INIT(); @@ -150,9 +176,7 @@ int main() { mrbc_create_task(tud, 0); tcb_rgb = mrbc_create_task(rgb_task, 0); create_sandbox(); - tcb_keymap = autoreload(); -//autoreload_state = AUTORELOAD_WAIT; -// mrbc_create_task(keymap, 0); + tcb_keymap = create_keymap_task(NULL); mrbc_run(); return 0; } diff --git a/src/msc_disk.c b/src/msc_disk.c index 23ac5be9..a47630a7 100644 --- a/src/msc_disk.c +++ b/src/msc_disk.c @@ -335,7 +335,7 @@ void tud_msc_write10_complete_cb(uint8_t lun) { (void)lun; -// autoreload_state = AUTORELOAD_READY; + autoreload_state = AUTORELOAD_READY; } //--------------------------------------------------------------------+ diff --git a/src/ruby/lib/tud.rb b/src/ruby/lib/tud.rb index e7060392..320d2c48 100644 --- a/src/ruby/lib/tud.rb +++ b/src/ruby/lib/tud.rb @@ -12,15 +12,18 @@ if $encoders && !$encoders.empty? $encoders.each { |encoder| encoder.read } end -# if autoreload_ready? -# if autoreload_tick == 0 -# puts "Autoreload is ready ..." -# autoreload_tick = 500 -# elsif autoreload_tick == 1 -# puts "Autoreload!" -# autoreload! -# autoreload_tick = 0 -# end -# autoreload_tick -= 1 -# end + if autoreload_ready? + if autoreload_tick == 0 + puts "Autoreload is ready ..." + autoreload_tick = 500 + elsif autoreload_tick == 1 + puts "Suspending keymap ..." + suspend_keymap + sleep_ms 100 + puts "Reloading keymap ..." + reload_keymap + puts "Reloaded keymap" + end + autoreload_tick -= 1 + end end diff --git a/src/ruby/sig/object.rbs b/src/ruby/sig/object.rbs index d2f3bcec..0e10d4d3 100644 --- a/src/ruby/sig/object.rbs +++ b/src/ruby/sig/object.rbs @@ -8,7 +8,8 @@ class Object def compile_ruby: (String) -> bool def invoke_ruby: () -> bool def autoreload_ready?: () -> bool - def autoreload!: () -> void + def suspend_keymap: () -> void + def reload_keymap: () -> void # Raspi def board_millis : -> Integer From 5a6897661191307224c3f5a193d8d246309e673f Mon Sep 17 00:00:00 2001 From: HASUMI Hitoshi <hasumikin@gmail.com> Date: Thu, 9 Sep 2021 23:12:43 +0900 Subject: [PATCH 09/10] wip --- CMakeLists.txt | 34 +++++++++++++++----- README.md | 28 +++++++++++++---- build/.gitignore | 3 ++ build/.keep | 0 src/main.c | 73 +++++++++++++++++++++++++------------------ src/msc_disk.c | 13 ++++---- src/msc_disk.h | 3 +- src/usb_descriptors.c | 8 +++++ src/version.h | 4 +-- 9 files changed, 111 insertions(+), 55 deletions(-) create mode 100644 build/.gitignore create mode 100644 build/.keep diff --git a/CMakeLists.txt b/CMakeLists.txt index 42c9762b..bd8f7d82 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,6 +14,9 @@ add_definitions( -DMAX_SYMBOLS_COUNT=500 -DMAX_VM_COUNT=10 ) +if(PRK_NO_MSC) + add_definitions(-DPRK_NO_MSC) +endif() # initialize the Raspberry Pi Pico SDK pico_sdk_init() @@ -28,8 +31,11 @@ set (PRK_BUILDDATE ${CMAKE_BUILDDATE}) set (PRK_REVISION ${CMAKE_REVISION}) configure_file ("${CMAKE_CURRENT_SOURCE_DIR}/src/version.h.in" "${CMAKE_CURRENT_SOURCE_DIR}/src/version.h") -set(PROJECT "prk_firmware-${PRK_VERSION}-${PRK_BUILDDATE}-${PRK_REVISION}") -#set(PROJECT "prk_firmware") # for test +if(PRK_NO_MSC) + set(PROJECT "prk_firmware-${PRK_VERSION}-${PRK_BUILDDATE}-no_msc") +else() + set(PROJECT "prk_firmware-${PRK_VERSION}-${PRK_BUILDDATE}-${PRK_REVISION}") +endif() add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/lib) @@ -48,7 +54,7 @@ pico_generate_pio_header(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/src/uart_tx.pio) pico_generate_pio_header(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}/src/ws2812.pio) set(RBC ${CMAKE_CURRENT_SOURCE_DIR}/lib/picoruby/build/bin/host-production/alloc_libc/picorbc) -#set(RBC RBENV_VERSION=mruby-3.0.0 mrbc) +#set(RBC RBENV_VERSION=mruby-3.0.0 mrbc) # In case of PicoRuby compiler enbugged 😵 add_custom_target(steep COMMAND bundle exec steep -h || bundle install @@ -56,6 +62,16 @@ add_custom_target(steep WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/src/ruby ) +if(PRK_NO_MSC) + add_custom_target(keymap + COMMAND ${RBC} -Bkeymap ${PROJECT_BINARY_DIR}/../keymap.rb + COMMAND mv ${PROJECT_BINARY_DIR}/../keymap.c ./ + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/src/ruby/lib + ) +else() + add_custom_target(keymap) +endif() + add_custom_target(ruby COMMAND ${RBC} -Bcore core.rb COMMAND ${RBC} -Bkeyboard keyboard.rb @@ -63,12 +79,10 @@ add_custom_target(ruby COMMAND ${RBC} -Brotary_encoder rotary_encoder.rb COMMAND ${RBC} -Btud tud.rb COMMAND ${RBC} -Brgb_task rgb_task.rb - COMMAND ${RBC} -Bkeymap ${PROJECT_BINARY_DIR}/../keymap.rb - COMMAND mv ${PROJECT_BINARY_DIR}/../keymap.c ./ WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/src/ruby/lib ) -add_custom_target(ruby_2 +add_custom_target(sandbox COMMAND ${RBC} -Bbuffer buffer.rb COMMAND ${RBC} -Bsandbox sandbox.rb WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/lib/picoruby/cli/ruby @@ -77,12 +91,16 @@ add_custom_target(ruby_2 add_dependencies(ruby steep ) - add_dependencies(${PROJECT} picoruby ruby - ruby_2 + sandbox ) +if(PRK_NO_MSC) + add_dependencies(${PROJECT} + keymap + ) +endif() target_link_libraries(${PROJECT} pico_stdlib diff --git a/README.md b/README.md index 3d0d0095..9ba26897 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ _(left: Raspberry Pi Pico / right: Sparkfun Pro Micro RP2040)_ - [ ] Asymmetrical type. eg) ??? - [x] UART communication between left and right - [ ] I2C communication between left and right -- [ ] Macros +- [x] Macros - [ ] Media keys - [x] RGBLED. An example on [picoruby/prk_crkbd](https://github.com/picoruby/prk_crkbd/blob/main/keymap.rb#L61-L76) - [ ] OLED display @@ -46,7 +46,7 @@ There are two ways to install PRK Fiwmware: Anyhow, you should: - Be knowledgeable how to install a UF2 file into Raspi Pico on [https://www.raspberrypi.org/documentation/rp2040/getting-started/#getting-started-with-c](https://www.raspberrypi.org/documentation/rp2040/getting-started/#getting-started-with-c) - - [https://learn.sparkfun.com/tutorials/pro-micro-rp2040-hookup-guide](https://learn.sparkfun.com/tutorials/pro-micro-rp2040-hookup-guide) will also help you if you use Sparkfun Pro Micro RP2040 + - [https://learn.sparkfun.com/tutorials/pro-micro-rp2040-hookup-guide](https://learn.sparkfun.com/tutorials/pro-micro-rp2040-hookup-guide) will also be helpful if you use Sparkfun Pro Micro RP2040 #### Using a release binary @@ -58,7 +58,11 @@ Anyhow, you should:  -- +- `PRKFirmware` mass storage drive should be mounted, then drag and drop your `keymap.rb` + +  + +Your keyboard will automatically reboot. Enjoy! #### Building a binary by yourself @@ -98,13 +102,25 @@ You may not want PRK Firmware to be a mass storage device in case that your empl (Defining PRK_NO_MSC macro will avoid implementing mass storage feature) - Now you should have `prk_firmware-[version]-[date]-[revision].uf2` file in `prk_firmware/keyboards/prk_meishi2/build/` directory which includes your keymap in code. + Now you should have `prk_firmware-[version]-[date]-no_msc.uf2` file in `prk_firmware/keyboards/prk_meishi2/build/` directory which includes your keymap in code. + +- Install that `.uf2` file into RP2040 -- Install .uf2 into RP2040 +### What if split type keyboard? + +- Make sure installing your setup on both side ### Contributing -Fork, clone, patch and send a pull request. +#### Building uf2 of excluding-keymap-version + +``` +cd prk_firmware/build +cmake -DCMAKE_BUILD_TYPE=Debug .. +make +``` + +Then patch and send a pull request. #### For those who are willing to contribute to PRK or write your own keymaps: diff --git a/build/.gitignore b/build/.gitignore new file mode 100644 index 00000000..edd99d5c --- /dev/null +++ b/build/.gitignore @@ -0,0 +1,3 @@ +* +!.keep +!.gitignore diff --git a/build/.keep b/build/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main.c b/src/main.c index 99d883b8..7ea4ed8b 100644 --- a/src/main.c +++ b/src/main.c @@ -27,7 +27,9 @@ /* tasks */ #include "ruby/lib/tud.c" #include "ruby/lib/rgb_task.c" +#ifdef PRK_NO_MSC #include "ruby/lib/keymap.c" +#endif void c_board_millis(mrb_vm *vm, mrb_value *v, int argc) @@ -47,26 +49,9 @@ c_rand(mrb_vm *vm, mrb_value *v, int argc) SET_INT_RETURN(rand()); } -#define MEMORY_SIZE (1024*200) - -static uint8_t memory_pool[MEMORY_SIZE]; +int autoreload_state; /* from msc_disk.h */ -void -mrbc_load_model(const uint8_t *mrb) -{ - mrbc_vm *vm = mrbc_vm_open(NULL); - if( vm == 0 ) { - console_printf("Error: Can't open VM.\n"); - return; - } - if( mrbc_load_mrb(vm, mrb) != 0 ) { - console_printf("Error: Illegal bytecode.\n"); - return; - } - mrbc_vm_begin(vm); - mrbc_vm_run(vm); - mrbc_raw_free(vm); -} +#ifndef PRK_NO_MSC #ifndef NODE_BOX_SIZE #define NODE_BOX_SIZE 50 @@ -114,14 +99,8 @@ create_keymap_task(mrbc_tcb *tcb) return tcb; } - - mrbc_tcb *tcb_keymap; -mrbc_tcb *tcb_rgb; /* from ws2812.h */ - -int autoreload_state; /* from msc_disk.h */ - void c_suspend_keymap(mrb_vm *vm, mrb_value *v, int argc) { @@ -144,24 +123,48 @@ c_autoreload_ready_q(mrb_vm *vm, mrb_value *v, int argc) } } +#endif /* PRK_NO_MSC */ + + +#define MEMORY_SIZE (1024*200) + +static uint8_t memory_pool[MEMORY_SIZE]; + +void +mrbc_load_model(const uint8_t *mrb) +{ + mrbc_vm *vm = mrbc_vm_open(NULL); + if( vm == 0 ) { + console_printf("Error: Can't open VM.\n"); + return; + } + if( mrbc_load_mrb(vm, mrb) != 0 ) { + console_printf("Error: Illegal bytecode.\n"); + return; + } + mrbc_vm_begin(vm); + mrbc_vm_run(vm); + mrbc_raw_free(vm); +} + +mrbc_tcb *tcb_rgb; /* from ws2812.h */ + int loglevel; int main() { loglevel = LOGLEVEL_WARN; - autoreload_state = AUTORELOAD_READY; stdio_init_all(); board_init(); tusb_init(); - msc_init(); mrbc_init(memory_pool, MEMORY_SIZE); mrbc_define_method(0, mrbc_class_object, "board_millis", c_board_millis); mrbc_define_method(0, mrbc_class_object, "rand", c_rand); - mrbc_define_method(0, mrbc_class_object, "autoreload_ready?", c_autoreload_ready_q); - mrbc_define_method(0, mrbc_class_object, "reload_keymap", c_reload_keymap); - mrbc_define_method(0, mrbc_class_object, "suspend_keymap", c_suspend_keymap); - MSC_INIT(); +#ifndef PRK_NO_MSC + msc_init(); +#endif + CDC_INIT(); GPIO_INIT(); TUD_INIT(); UART_INIT(); @@ -176,7 +179,15 @@ int main() { mrbc_create_task(tud, 0); tcb_rgb = mrbc_create_task(rgb_task, 0); create_sandbox(); +#ifdef PRK_NO_MSC + mrbc_create_task(keymap, 0); +#else + mrbc_define_method(0, mrbc_class_object, "autoreload_ready?", c_autoreload_ready_q); + mrbc_define_method(0, mrbc_class_object, "reload_keymap", c_reload_keymap); + mrbc_define_method(0, mrbc_class_object, "suspend_keymap", c_suspend_keymap); + autoreload_state = AUTORELOAD_READY; tcb_keymap = create_keymap_task(NULL); +#endif mrbc_run(); return 0; } diff --git a/src/msc_disk.c b/src/msc_disk.c index a47630a7..b381e408 100644 --- a/src/msc_disk.c +++ b/src/msc_disk.c @@ -23,15 +23,15 @@ * */ -#include "bsp/board.h" -#include "tusb.h" - -#if CFG_TUD_MSC - #include "hardware/flash.h" #include "hardware/sync.h" +#include "bsp/board.h" + +#include "tusb.h" #include "msc_disk.h" +#include <mrubyc.h> + // whether host does safe-eject static bool ejected = false; @@ -338,6 +338,7 @@ tud_msc_write10_complete_cb(uint8_t lun) autoreload_state = AUTORELOAD_READY; } + //--------------------------------------------------------------------+ // USB CDC //--------------------------------------------------------------------+ @@ -387,5 +388,3 @@ void tud_cdc_rx_cb(uint8_t itf) { (void) itf; } - -#endif diff --git a/src/msc_disk.h b/src/msc_disk.h index 8048c58f..7b7db483 100644 --- a/src/msc_disk.h +++ b/src/msc_disk.h @@ -39,6 +39,7 @@ void msc_findDirEnt(const char *filename, DirEnt *entry); void c_cdc_task(mrb_vm *vm, mrb_value *v, int argc); -#define MSC_INIT() do { \ +#define CDC_INIT() do { \ mrbc_define_method(0, mrbc_class_object, "cdc_task", c_cdc_task); \ } while (0) + diff --git a/src/usb_descriptors.c b/src/usb_descriptors.c index b98a51b1..19d4cbcc 100644 --- a/src/usb_descriptors.c +++ b/src/usb_descriptors.c @@ -76,7 +76,11 @@ uint8_t const * tud_descriptor_device_cb(void) // Configuration Descriptor //--------------------------------------------------------------------+ +#ifdef PRK_NO_MSC +#define CONFIG_TOTAL_LEN (TUD_CONFIG_DESC_LEN + TUD_CDC_DESC_LEN + TUD_HID_INOUT_DESC_LEN) +#else #define CONFIG_TOTAL_LEN (TUD_CONFIG_DESC_LEN + TUD_CDC_DESC_LEN + TUD_MSC_DESC_LEN + TUD_HID_INOUT_DESC_LEN) +#endif #if CFG_TUSB_MCU == OPT_MCU_LPC175X_6X || CFG_TUSB_MCU == OPT_MCU_LPC177X_8X || CFG_TUSB_MCU == OPT_MCU_LPC40XX // LPC 17xx and 40xx endpoint type (bulk/interrupt/iso) are fixed by its number @@ -134,7 +138,9 @@ enum ITF_NUM_CDC = 0, ITF_NUM_CDC_DATA, ITF_NUM_HID, +#ifndef PRK_NO_MSC ITF_NUM_MSC, +#endif ITF_NUM_TOTAL }; @@ -146,8 +152,10 @@ uint8_t const desc_fs_configuration[] = // Interface number, string index, EP notification address and size, EP data address (out, in) and size. TUD_CDC_DESCRIPTOR(ITF_NUM_CDC, 4, EPNUM_CDC_NOTIF, 8, EPNUM_CDC_OUT, EPNUM_CDC_IN, 64), +#ifndef PRK_NO_MSC // Interface number, string index, EP Out & EP In address, EP size TUD_MSC_DESCRIPTOR(ITF_NUM_MSC, 5, EPNUM_MSC_OUT, EPNUM_MSC_IN, 64), +#endif // Interface number, string index, protocol, report descriptor len, EP In address, size & polling interval TUD_HID_INOUT_DESCRIPTOR(ITF_NUM_HID, 0, 0, sizeof(desc_hid_report), EPNUM_HID_OUT, EPNUM_HID_IN, 64, 0x08), diff --git a/src/version.h b/src/version.h index 4652a730..c327e565 100644 --- a/src/version.h +++ b/src/version.h @@ -1,5 +1,5 @@ #pragma once #define PRK_VERSION "0.9.0" -#define PRK_BUILDDATE "20210908" -#define PRK_REVISION "e8ee115" +#define PRK_BUILDDATE "20210909" +#define PRK_REVISION "6e77670" From a1b573fdfe35ec458a1f7da971f28b3728f7e3ac Mon Sep 17 00:00:00 2001 From: HASUMI Hitoshi <hasumikin@gmail.com> Date: Fri, 10 Sep 2021 01:01:47 +0900 Subject: [PATCH 10/10] Improve cmake to make zip and gz --- CMakeLists.txt | 7 +++++++ src/version.h | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index bd8f7d82..70d3143f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -122,3 +122,10 @@ include_directories(${PROJECT} # create map/bin/hex/uf2 file in addition to ELF. pico_add_extra_outputs(${PROJECT}) + +add_custom_command( + TARGET ${PROJECT} + POST_BUILD + COMMAND gzip -k ${PROJECT}.uf2 + COMMAND zip -r ${PROJECT}.uf2.zip ${PROJECT}.uf2 +) diff --git a/src/version.h b/src/version.h index c327e565..d7e3bbcf 100644 --- a/src/version.h +++ b/src/version.h @@ -1,5 +1,5 @@ #pragma once #define PRK_VERSION "0.9.0" -#define PRK_BUILDDATE "20210909" -#define PRK_REVISION "6e77670" +#define PRK_BUILDDATE "20210910" +#define PRK_REVISION "5a68976"