From 5b762d016841acb3cefa60f05e963711f0a3eb2b Mon Sep 17 00:00:00 2001 From: Edward Shin Date: Sun, 15 Oct 2023 00:36:49 -0400 Subject: [PATCH] USB Improvements * Introduce shell module for basic serial shell with argument parsing * Introduce shell_cmd_list module for basic compile-time command registration * Harden USB handling to hang less and drop fewer inputs - Service tud_task() with periodic TC0 timer interrupt - Service cdc_task() with periodic TC1 timer interrupt - Handle shell servicing in main app loop - Add a circular buffering layer for reads/writes * Change newline prints to also send carriage return * Refactor filesystem commands for shell subsystem * Introduce new shell commands: - 'help' command - 'flash' command to reset into bootloader - 'stress' command to stress CDC writes Testing: * Shell validated on Sensor Watch Blue w/ Linux host * Shell validated in emscripten emulator * Tuned by spamming inputs during `stress` cmd until stack didn't crash --- make.mk | 1 + movement/filesystem.c | 121 +++++----- movement/filesystem.h | 9 +- movement/make/Makefile | 2 + movement/movement.c | 32 +-- movement/shell.c | 220 ++++++++++++++++++ movement/shell.h | 34 +++ movement/shell_cmd_list.c | 159 +++++++++++++ movement/shell_cmd_list.h | 38 +++ watch-library/hardware/main.c | 1 - watch-library/hardware/watch/watch_private.c | 145 +++++++----- .../hardware/watch/watch_private_cdc.c | 160 +++++++++++++ .../hardware/watch/watch_private_cdc.h | 33 +++ watch-library/shared/watch/watch.h | 6 +- watch-library/shared/watch/watch_private.h | 19 +- 15 files changed, 820 insertions(+), 160 deletions(-) create mode 100644 movement/shell.c create mode 100644 movement/shell.h create mode 100644 movement/shell_cmd_list.c create mode 100644 movement/shell_cmd_list.h create mode 100644 watch-library/hardware/watch/watch_private_cdc.c create mode 100644 watch-library/hardware/watch/watch_private_cdc.h diff --git a/make.mk b/make.mk index 955ea3102..891f9363e 100644 --- a/make.mk +++ b/make.mk @@ -121,6 +121,7 @@ SRCS += \ $(TOP)/watch-library/hardware/watch/watch_storage.c \ $(TOP)/watch-library/hardware/watch/watch_deepsleep.c \ $(TOP)/watch-library/hardware/watch/watch_private.c \ + $(TOP)/watch-library/hardware/watch/watch_private_cdc.c \ $(TOP)/watch-library/hardware/watch/watch.c \ $(TOP)/watch-library/hardware/hal/src/hal_atomic.c \ $(TOP)/watch-library/hardware/hal/src/hal_delay.c \ diff --git a/movement/filesystem.c b/movement/filesystem.c index 97e35455d..9df0a8d29 100644 --- a/movement/filesystem.c +++ b/movement/filesystem.c @@ -100,7 +100,7 @@ static int filesystem_ls(lfs_t *lfs, const char *path) { printf("%4ld bytes ", info.size); - printf("%s\n", info.name); + printf("%s\r\n", info.name); } err = lfs_dir_close(lfs, &dir); @@ -117,11 +117,11 @@ bool filesystem_init(void) { // reformat if we can't mount the filesystem // this should only happen on the first boot if (err < 0) { - printf("Ignore that error! Formatting filesystem...\n"); + printf("Ignore that error! Formatting filesystem...\r\n"); err = lfs_format(&lfs, &cfg); if (err < 0) return false; err = lfs_mount(&lfs, &cfg) == LFS_ERR_OK; - printf("Filesystem mounted with %ld bytes free.\n", filesystem_get_free_space()); + printf("Filesystem mounted with %ld bytes free.\r\n", filesystem_get_free_space()); } return err == LFS_ERR_OK; @@ -139,7 +139,7 @@ bool filesystem_rm(char *filename) { if (filesystem_file_exists(filename)) { return lfs_remove(&lfs, filename) == LFS_ERR_OK; } else { - printf("rm: %s: No such file\n", filename); + printf("rm: %s: No such file\r\n", filename); return false; } } @@ -197,13 +197,13 @@ static void filesystem_cat(char *filename) { char *buf = malloc(info.size + 1); filesystem_read_file(filename, buf, info.size); buf[info.size] = '\0'; - printf("%s\n", buf); + printf("%s\r\n", buf); free(buf); } else { - printf("\n"); + printf("\r\n"); } } else { - printf("cat: %s: No such file\n", filename); + printf("cat: %s: No such file\r\n", filename); } } @@ -223,59 +223,60 @@ bool filesystem_append_file(char *filename, char *text, int32_t length) { return lfs_file_close(&lfs, &file) == LFS_ERR_OK; } -void filesystem_process_command(char *line) { - printf("$ %s", line); - char *command = strtok(line, " \n"); - - if (strcmp(command, "ls") == 0) { - char *directory = strtok(NULL, " \n"); - if (directory == NULL) { - filesystem_ls(&lfs, "/"); - } else { - printf("usage: ls\n"); - } - } else if (strcmp(command, "cat") == 0) { - char *filename = strtok(NULL, " \n"); - if (filename == NULL) { - printf("usage: cat file\n"); - } else { - filesystem_cat(filename); - } - } else if (strcmp(command, "df") == 0) { - printf("free space: %ld bytes\n", filesystem_get_free_space()); - } else if (strcmp(command, "rm") == 0) { - char *filename = strtok(NULL, " \n"); - if (filename == NULL) { - printf("usage: rm file\n"); - } else { - filesystem_rm(filename); - } - } else if (strcmp(command, "echo") == 0) { - char *text = malloc(248); - memset(text, 0, 248); - size_t pos = 0; - char *word = strtok(NULL, " \n"); - while (strcmp(word, ">") && strcmp(word, ">>")) { - sprintf(text + pos, "%s ", word); - pos += strlen(word) + 1; - word = strtok(NULL, " \n"); - if (word == NULL) break; - } - text[strlen(text) - 1] = 0; - char *filename = strtok(NULL, " \n"); - if (filename == NULL) { - printf("usage: echo text > file\n"); - } else if (strchr(filename, '/') || strchr(filename, '\\')) { - printf("subdirectories are not supported\n"); - } else if (!strcmp(word, ">")) { - filesystem_write_file(filename, text, strlen(text)); - filesystem_append_file(filename, "\n", 1); - } else if (!strcmp(word, ">>")) { - filesystem_append_file(filename, text, strlen(text)); - filesystem_append_file(filename, "\n", 1); - } - free(text); +int filesystem_cmd_ls(int argc, char *argv[]) { + if (argc >= 2) { + filesystem_ls(&lfs, argv[1]); } else { - printf("%s: command not found\n", command); + filesystem_ls(&lfs, "/"); } + return 0; } + +int filesystem_cmd_cat(int argc, char *argv[]) { + (void) argc; + filesystem_cat(argv[1]); + return 0; +} + +int filesystem_cmd_df(int argc, char *argv[]) { + (void) argc; + (void) argv; + printf("free space: %ld bytes\r\n", filesystem_get_free_space()); + return 0; +} + +int filesystem_cmd_rm(int argc, char *argv[]) { + (void) argc; + filesystem_rm(argv[1]); + return 0; +} + +int filesystem_cmd_echo(int argc, char *argv[]) { + (void) argc; + + char *line = argv[1]; + size_t line_len = strlen(line); + if (line[0] == '"' || line[0] == '\'') { + line++; + line_len -= 2; + line[line_len] = '\0'; + } + + if (strchr(argv[3], '/')) { + printf("subdirectories are not supported\r\n"); + return -2; + } + + if (!strcmp(argv[2], ">")) { + filesystem_write_file(argv[3], line, line_len); + filesystem_append_file(argv[3], "\n", 1); + } else if (!strcmp(argv[2], ">>")) { + filesystem_append_file(argv[3], line, line_len); + filesystem_append_file(argv[3], "\n", 1); + } else { + return -2; + } + + return 0; +} + diff --git a/movement/filesystem.h b/movement/filesystem.h index 3cd3d0921..fa3d9d1ad 100644 --- a/movement/filesystem.h +++ b/movement/filesystem.h @@ -96,9 +96,10 @@ bool filesystem_write_file(char *filename, char *text, int32_t length); */ bool filesystem_append_file(char *filename, char *text, int32_t length); -/** @brief Handles the interactive file browser when Movement is plugged in to USB. - * @param line The command that the user typed into the serial console. - */ -void filesystem_process_command(char *line); +int filesystem_cmd_ls(int argc, char *argv[]); +int filesystem_cmd_cat(int argc, char *argv[]); +int filesystem_cmd_df(int argc, char *argv[]); +int filesystem_cmd_rm(int argc, char *argv[]); +int filesystem_cmd_echo(int argc, char *argv[]); #endif // FILESYSTEM_H_ diff --git a/movement/make/Makefile b/movement/make/Makefile index 512f2ea80..cc6a32de5 100644 --- a/movement/make/Makefile +++ b/movement/make/Makefile @@ -49,6 +49,8 @@ SRCS += \ ../../littlefs/lfs_util.c \ ../movement.c \ ../filesystem.c \ + ../shell.c \ + ../shell_cmd_list.c \ ../watch_faces/clock/simple_clock_face.c \ ../watch_faces/clock/world_clock_face.c \ ../watch_faces/clock/beats_face.c \ diff --git a/movement/movement.c b/movement/movement.c index f0868416e..b6c119786 100644 --- a/movement/movement.c +++ b/movement/movement.c @@ -33,6 +33,7 @@ #include "watch.h" #include "filesystem.h" #include "movement.h" +#include "shell.h" #ifndef MOVEMENT_FIRMWARE #include "movement_config.h" @@ -538,30 +539,9 @@ bool app_loop(void) { } } - // if we are plugged into USB, handle the file browser tasks + // if we are plugged into USB, handle the serial shell if (watch_is_usb_enabled()) { - char line[256] = {0}; -#if __EMSCRIPTEN__ - // This is a terrible hack; ideally this should be handled deeper in the watch library. - // Alas, emscripten treats read() as something that should pop up an input box, so I - // wasn't able to implement this over there. I sense that this relates to read() being - // the wrong way to read data from USB (like we should be using fgets or something), but - // until I untangle that, this will have to do. - char *received_data = (char*)EM_ASM_INT({ - var len = lengthBytesUTF8(tx) + 1; - var s = _malloc(len); - stringToUTF8(tx, s, len); - return s; - }); - memcpy(line, received_data, min(255, strlen(received_data))); - free(received_data); - EM_ASM({ - tx = ""; - }); -#else - read(0, line, 256); -#endif - if (strlen(line)) filesystem_process_command(line); + shell_task(); } event.subsecond = 0; @@ -633,13 +613,13 @@ void cb_fast_tick(void) { // Notice: is it possible that two or more buttons have an identical timestamp? In this case // only one of these buttons would receive the long press event. Don't bother for now... if (movement_state.light_down_timestamp > 0) - if (movement_state.fast_ticks - movement_state.light_down_timestamp == MOVEMENT_LONG_PRESS_TICKS + 1) + if (movement_state.fast_ticks - movement_state.light_down_timestamp == MOVEMENT_LONG_PRESS_TICKS + 1) event.event_type = EVENT_LIGHT_LONG_PRESS; if (movement_state.mode_down_timestamp > 0) - if (movement_state.fast_ticks - movement_state.mode_down_timestamp == MOVEMENT_LONG_PRESS_TICKS + 1) + if (movement_state.fast_ticks - movement_state.mode_down_timestamp == MOVEMENT_LONG_PRESS_TICKS + 1) event.event_type = EVENT_MODE_LONG_PRESS; if (movement_state.alarm_down_timestamp > 0) - if (movement_state.fast_ticks - movement_state.alarm_down_timestamp == MOVEMENT_LONG_PRESS_TICKS + 1) + if (movement_state.fast_ticks - movement_state.alarm_down_timestamp == MOVEMENT_LONG_PRESS_TICKS + 1) event.event_type = EVENT_ALARM_LONG_PRESS; // this is just a fail-safe; fast tick should be disabled as soon as the button is up, the LED times out, and/or the alarm finishes. // but if for whatever reason it isn't, this forces the fast tick off after 20 seconds. diff --git a/movement/shell.c b/movement/shell.c new file mode 100644 index 000000000..8f146e597 --- /dev/null +++ b/movement/shell.c @@ -0,0 +1,220 @@ +/* + * MIT License + * + * Copyright (c) 2023 Edward Shin + * + * 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 "shell.h" + +#include +#include +#include +#include +#include +#include + +#if __EMSCRIPTEN__ +#include +#endif + +#include "watch.h" +#include "shell_cmd_list.h" + +extern shell_command_t g_shell_commands[]; +extern const size_t g_num_shell_commands; + +#define NEWLINE "\r\n" + +#define SHELL_BUF_SZ (256) +#define SHELL_MAX_ARGS (16) +#define SHELL_PROMPT "swsh> " + +static char s_buf[SHELL_BUF_SZ] = {0}; +static size_t s_buf_len = 0; +// Pointer to the first invalid byte after the end of input. +static char *const s_buf_end = s_buf + SHELL_BUF_SZ; + +static char *prv_skip_whitespace(char *c) { + while (c >= s_buf && c < s_buf_end) { + if (*c == 0) { + return NULL; + } + if (!isspace((int) *c) != 0) { + return c; + } + c++; + } + return NULL; +} + +static char *prv_skip_non_whitespace(char *c) { + bool in_quote = false; + char quote_char; + while (c >= s_buf && c < s_buf_end) { + if (*c == 0) { + return NULL; + } + // Basic handling of quoted arguments. + // Can't handle recursive quotes. :( + if (in_quote || *c == '"' || *c == '\'') { + if (!in_quote) { + quote_char = *c; + in_quote = true; + } else if (*c == quote_char) { + in_quote = false; + } + } else { + if (isspace((int) *c) != 0) { + return c; + } + } + c++; + } + return NULL; +} + +static int prv_handle_command() { + char *argv[SHELL_MAX_ARGS] = {0}; + int argc = 0; + + char *c = &s_buf[0]; + s_buf[SHELL_BUF_SZ - 1] = '\0'; + + while (argc < SHELL_MAX_ARGS) { + // Skip contiguous whitespace + c = prv_skip_whitespace(c); + if (c == NULL) { + // Reached end of buffer + break; + } + + // We hit non-whitespace, set argv and argc for this upcoming argument + argv[argc++] = c; + + // Skip contiguous non-whitespace + c = prv_skip_non_whitespace(c); + if (c == NULL) { + // Reached end of buffer + break; + } + + // NULL-terminate this arg string and then increment. + *(c++) = '\0'; + } + + if (argc == 0) { + return -1; + } + + // Match against the command list + for (size_t i = 0; i < g_num_shell_commands; i++) { + if (!strcasecmp(g_shell_commands[i].name, argv[0])) { + // If argc isn't valid for this command, display its help instead. + if (((argc - 1) < g_shell_commands[i].min_args) || + ((argc - 1) > g_shell_commands[i].max_args)) { + if (g_shell_commands[i].help != NULL) { + printf(NEWLINE "%s" NEWLINE, g_shell_commands[i].help); + } + return -2; + } + // Call the command's callback + if (g_shell_commands[i].cb != NULL) { + printf(NEWLINE); + int ret = g_shell_commands[i].cb(argc, argv); + if (ret == -2) { + printf(NEWLINE "%s" NEWLINE, g_shell_commands[i].help); + } + return ret; + } + } + } + + return -1; +} + +void shell_task(void) { +#if __EMSCRIPTEN__ + // This is a terrible hack; ideally this should be handled deeper in the watch library. + // Alas, emscripten treats read() as something that should pop up an input box, so I + // wasn't able to implement this over there. I sense that this relates to read() being + // the wrong way to read data from USB (like we should be using fgets or something), but + // until I untangle that, this will have to do. + char *received_data = (char*)EM_ASM_INT({ + var len = lengthBytesUTF8(tx) + 1; + var s = _malloc(len); + stringToUTF8(tx, s, len); + return s; + }); + s_buf_len = min((SHELL_BUF_SZ - 2), strlen(received_data)); + memcpy(s_buf, received_data, s_buf_len); + free(received_data); + s_buf[s_buf_len++] = '\n'; + s_buf[s_buf_len++] = '\0'; + prv_handle_command(); + EM_ASM({ + tx = ""; + }); +#else + // Read one character at a time until we run out. + while (true) { + if (s_buf_len >= (SHELL_BUF_SZ - 1)) { + printf(NEWLINE "Command too long, clearing."); + printf(NEWLINE SHELL_PROMPT); + s_buf_len = 0; + break; + } + + int c = getchar(); + + if (c < 0) { + // Nothing left to read, we're done. + break; + } + + if (c == '\b') { + // Handle backspace character. + // We need to emit a backspace, overwrite the character on the + // screen with a space, and then backspace again to move the cursor. + if (s_buf_len > 0) { + printf("\b \b"); + s_buf_len--; + } + continue; + } else if (c != '\n' && c != '\r') { + // Print regular characters to the screen. + putchar(c); + } + + s_buf[s_buf_len] = c; + + if (c == '\n' || c == '\r') { + // Newline! Handle the command. + s_buf[s_buf_len+1] = '\0'; + (void) prv_handle_command(); + s_buf_len = 0; + printf(NEWLINE SHELL_PROMPT); + break; + } else { + s_buf_len++; + } + } +#endif +} diff --git a/movement/shell.h b/movement/shell.h new file mode 100644 index 000000000..27dbf6727 --- /dev/null +++ b/movement/shell.h @@ -0,0 +1,34 @@ +/* + * MIT License + * + * Copyright (c) 2023 Edward Shin + * + * 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 SHELL_H_ +#define SHELL_H_ + +/** @brief Called periodically from the app loop to handle shell commands. + * When a full command is complete, parses and executes its matching + * callback. + */ +void shell_task(void); + +#endif diff --git a/movement/shell_cmd_list.c b/movement/shell_cmd_list.c new file mode 100644 index 000000000..0ea08a56e --- /dev/null +++ b/movement/shell_cmd_list.c @@ -0,0 +1,159 @@ +/* + * MIT License + * + * Copyright (c) 2023 Edward Shin + * + * 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 "shell_cmd_list.h" + +#include +#include +#include + +#include "filesystem.h" +#include "watch.h" + +static int help_cmd(int argc, char *argv[]); +static int flash_cmd(int argc, char *argv[]); +static int stress_cmd(int argc, char *argv[]); + +shell_command_t g_shell_commands[] = { + { + .name = "?", + .help = "print command list", + .min_args = 0, + .max_args = 0, + .cb = help_cmd, + }, + { + .name = "help", + .help = "print command list", + .min_args = 0, + .max_args = 0, + .cb = help_cmd, + }, + { + .name = "flash", + .help = "reboot to UF2 bootloader", + .min_args = 0, + .max_args = 0, + .cb = flash_cmd, + }, + { + .name = "ls", + .help = "usage: ls [PATH]", + .min_args = 0, + .max_args = 1, + .cb = filesystem_cmd_ls, + }, + { + .name = "cat", + .help = "usage: cat ", + .min_args = 1, + .max_args = 1, + .cb = filesystem_cmd_cat, + }, + { + .name = "df", + .help = "print filesystem free space", + .min_args = 0, + .max_args = 0, + .cb = filesystem_cmd_df, + }, + { + .name = "rm", + .help = "usage: rm [PATH]", + .min_args = 1, + .max_args = 1, + .cb = filesystem_cmd_rm, + }, + { + .name = "echo", + .help = "usage: echo TEXT {>,>>} FILE", + .min_args = 3, + .max_args = 3, + .cb = filesystem_cmd_echo, + }, + { + .name = "stress", + .help = "test CDC write; usage: stress [LEN] [DELAY_MS]", + .min_args = 0, + .max_args = 2, + .cb = stress_cmd, + }, +}; + +const size_t g_num_shell_commands = sizeof(g_shell_commands) / sizeof(shell_command_t); + +static int help_cmd(int argc, char *argv[]) { + (void) argc; + (void) argv; + + printf("Command List:\r\n"); + for (size_t i = 0; i < g_num_shell_commands; i++) { + printf(" %s\t%s\r\n", + g_shell_commands[i].name, + (g_shell_commands[i].help) ? g_shell_commands[i].help : "" + ); + } + + return 0; +} + +static int flash_cmd(int argc, char *argv[]) { + (void) argc; + (void) argv; + + watch_reset_to_bootloader(); + return 0; +} + +#define STRESS_CMD_MAX_LEN (512) +static int stress_cmd(int argc, char *argv[]) { + char test_str[STRESS_CMD_MAX_LEN+1] = {0}; + + int max_len = 512; + int delay = 0; + + if (argc >= 2) { + if ((max_len = atoi(argv[1])) == 0) { + return -1; + } + if (max_len > 512) { + return -1; + } + } + + if (argc >= 3) { + delay = atoi(argv[2]); + } + + for (int i = 0; i < max_len; i++) { + snprintf(&test_str[i], 2, "%u", (i+1)%10); + printf("%u:\t%s\r\n", (i+1), test_str); + if (delay > 0) { + delay_ms(delay); + } + } + + return 0; +} + diff --git a/movement/shell_cmd_list.h b/movement/shell_cmd_list.h new file mode 100644 index 000000000..89031a542 --- /dev/null +++ b/movement/shell_cmd_list.h @@ -0,0 +1,38 @@ +/* + * MIT License + * + * Copyright (c) 2023 Edward Shin + * + * 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 SHELL_CMD_LIST_H_ +#define SHELL_CMD_LIST_H_ + +#include + +typedef struct { + const char *name; // Name used to invoke the command + const char *help; // Help string + int8_t min_args; // Minimum number of arguments (_excluding_ the command name) + int8_t max_args; // Maximum number of arguments (_excluding_ the command name) + int (*cb)(int argc, char *argv[]); // Callback for the command +} shell_command_t; + +#endif diff --git a/watch-library/hardware/main.c b/watch-library/hardware/main.c index 325610f6e..8ac4fca66 100755 --- a/watch-library/hardware/main.c +++ b/watch-library/hardware/main.c @@ -79,7 +79,6 @@ int main(void) { while (1) { bool usb_enabled = hri_usbdevice_get_CTRLA_ENABLE_bit(USB); bool can_sleep = app_loop(); - if (can_sleep && !usb_enabled) { app_prepare_for_standby(); sleep(4); diff --git a/watch-library/hardware/watch/watch_private.c b/watch-library/hardware/watch/watch_private.c index cd607b8e8..e6db33e7e 100644 --- a/watch-library/hardware/watch/watch_private.c +++ b/watch-library/hardware/watch/watch_private.c @@ -23,6 +23,7 @@ */ #include "watch_private.h" +#include "watch_private_cdc.h" #include "watch_utility.h" #include "tusb.h" @@ -170,6 +171,87 @@ void _watch_disable_tcc(void) { // disable the TCC hri_tcc_clear_CTRLA_ENABLE_bit(TCC0); hri_mclk_clear_APBCMASK_TCC0_bit(MCLK); +} + +void _watch_enable_tc0(void) { + // before we init TinyUSB, we are going to need a periodic callback to handle TinyUSB tasks. + // TC2 and TC3 are reserved for devices on the 9-pin connector, so let's use TC0. + // clock TC0 with the 8 MHz clock on GCLK0. + hri_gclk_write_PCHCTRL_reg(GCLK, TC0_GCLK_ID, GCLK_PCHCTRL_GEN_GCLK0_Val | GCLK_PCHCTRL_CHEN); + // and enable the peripheral clock. + hri_mclk_set_APBCMASK_TC0_bit(MCLK); + // disable and reset TC0. + hri_tc_clear_CTRLA_ENABLE_bit(TC0); + hri_tc_wait_for_sync(TC0, TC_SYNCBUSY_ENABLE); + hri_tc_write_CTRLA_reg(TC0, TC_CTRLA_SWRST); + hri_tc_wait_for_sync(TC0, TC_SYNCBUSY_SWRST); + hri_tc_write_CTRLA_reg(TC0, TC_CTRLA_PRESCALER_DIV1024 | // divide the 8 MHz clock by 1024 to count at 7812.5 Hz + TC_CTRLA_MODE_COUNT8 | // count in 8-bit mode + TC_CTRLA_RUNSTDBY); // run in standby, just in case we figure that out + hri_tccount8_write_PER_reg(TC0, 10); // 7812.5 Hz / 10 = 781.125 Hz + // set an interrupt on overflow; this will call TC0_Handler below. + hri_tc_set_INTEN_OVF_bit(TC0); + + // set priority higher than TC1 + NVIC_SetPriority(TC0_IRQn, 5); + NVIC_ClearPendingIRQ(TC0_IRQn); + NVIC_EnableIRQ(TC0_IRQn); + + // Start the timer + hri_tc_set_CTRLA_ENABLE_bit(TC0); +} + +void _watch_disable_tc0(void) { + NVIC_DisableIRQ(TC0_IRQn); + NVIC_ClearPendingIRQ(TC0_IRQn); + hri_tc_clear_CTRLA_ENABLE_bit(TC0); + hri_tc_wait_for_sync(TC0, TC_SYNCBUSY_ENABLE); + hri_tc_write_CTRLA_reg(TC0, TC_CTRLA_SWRST); + hri_tc_wait_for_sync(TC0, TC_SYNCBUSY_SWRST); +} + +void _watch_enable_tc1(void) { + hri_gclk_write_PCHCTRL_reg(GCLK, TC1_GCLK_ID, GCLK_PCHCTRL_GEN_GCLK0_Val | GCLK_PCHCTRL_CHEN); + // and enable the peripheral clock. + hri_mclk_set_APBCMASK_TC1_bit(MCLK); + // disable and reset TC1. + hri_tc_clear_CTRLA_ENABLE_bit(TC1); + hri_tc_wait_for_sync(TC1, TC_SYNCBUSY_ENABLE); + hri_tc_write_CTRLA_reg(TC1, TC_CTRLA_SWRST); + hri_tc_wait_for_sync(TC1, TC_SYNCBUSY_SWRST); + hri_tc_write_CTRLA_reg(TC1, TC_CTRLA_PRESCALER_DIV1024 | // divide the 8 MHz clock by 1024 to count at 7812.5 Hz + TC_CTRLA_MODE_COUNT8 | // count in 8-bit mode + TC_CTRLA_RUNSTDBY); // run in standby, just in case we figure that out + hri_tccount8_write_PER_reg(TC1, 20); // 7812.5 Hz / 50 = 156.25 Hz + // set an interrupt on overflow; this will call TC1_Handler below. + hri_tc_set_INTEN_OVF_bit(TC1); + + // set priority lower than TC0 + NVIC_SetPriority(TC1_IRQn, 6); + NVIC_ClearPendingIRQ(TC1_IRQn); + NVIC_EnableIRQ(TC1_IRQn); + + // Start the timer + hri_tc_set_CTRLA_ENABLE_bit(TC1); +} + +void _watch_disable_tc1(void) { + NVIC_DisableIRQ(TC1_IRQn); + NVIC_ClearPendingIRQ(TC1_IRQn); + hri_tc_clear_CTRLA_ENABLE_bit(TC1); + hri_tc_wait_for_sync(TC1, TC_SYNCBUSY_ENABLE); + hri_tc_write_CTRLA_reg(TC1, TC_CTRLA_SWRST); + hri_tc_wait_for_sync(TC1, TC_SYNCBUSY_SWRST); +} + +void TC0_Handler(void) { + tud_task(); + TC0->COUNT8.INTFLAG.reg |= TC_INTFLAG_OVF; +} + +void TC1_Handler(void) { + cdc_task(); + TC1->COUNT8.INTFLAG.reg |= TC_INTFLAG_OVF; } void _watch_enable_usb(void) { @@ -216,76 +298,17 @@ void _watch_enable_usb(void) { gpio_set_pin_function(PIN_PA24, PINMUX_PA24G_USB_DM); gpio_set_pin_function(PIN_PA25, PINMUX_PA25G_USB_DP); - // before we init TinyUSB, we are going to need a periodic callback to handle TinyUSB tasks. - // TC2 and TC3 are reserved for devices on the 9-pin connector, so let's use TC0. - // clock TC0 with the 8 MHz clock on GCLK0. - hri_gclk_write_PCHCTRL_reg(GCLK, TC0_GCLK_ID, GCLK_PCHCTRL_GEN_GCLK0_Val | GCLK_PCHCTRL_CHEN); - // and enable the peripheral clock. - hri_mclk_set_APBCMASK_TC0_bit(MCLK); - // disable and reset TC0. - hri_tc_clear_CTRLA_ENABLE_bit(TC0); - hri_tc_wait_for_sync(TC0, TC_SYNCBUSY_ENABLE); - hri_tc_write_CTRLA_reg(TC0, TC_CTRLA_SWRST); - hri_tc_wait_for_sync(TC0, TC_SYNCBUSY_SWRST); - // configure the TC to overflow 1,000 times per second - hri_tc_write_CTRLA_reg(TC0, TC_CTRLA_PRESCALER_DIV64 | // divide the 8 MHz clock by 64 to count at 125 KHz - TC_CTRLA_MODE_COUNT8 | // count in 8-bit mode - TC_CTRLA_RUNSTDBY); // run in standby, just in case we figure that out - hri_tccount8_write_PER_reg(TC0, 125); // 125000 Hz / 125 = 1,000 Hz - // set an interrupt on overflow; this will call TC0_Handler below. - hri_tc_set_INTEN_OVF_bit(TC0); - NVIC_ClearPendingIRQ(TC0_IRQn); - NVIC_EnableIRQ (TC0_IRQn); + _watch_enable_tc0(); - // now we can init TinyUSB tusb_init(); - // and start the timer that handles USB device tasks. - hri_tc_set_CTRLA_ENABLE_bit(TC0); -} -// this function ends up getting called by printf to log stuff to the USB console. -int _write(int file, char *ptr, int len) { - (void)file; - if (hri_usbdevice_get_CTRLA_ENABLE_bit(USB)) { - tud_cdc_n_write(0, (void const*)ptr, len); - tud_cdc_n_write_flush(0); - return len; - } - - return 0; -} - -static char buf[256] = {0}; - -int _read(int file, char *ptr, int len) { - (void)file; - int actual_length = strlen(buf); - if (actual_length) { - memcpy(ptr, buf, min(len, actual_length)); - return actual_length; - } - return 0; + _watch_enable_tc1(); } void USB_Handler(void) { tud_int_handler(0); } -static void cdc_task(void) { - if (tud_cdc_n_available(0)) { - tud_cdc_n_read(0, buf, sizeof(buf)); - } else { - memset(buf, 0, 256); - } -} - -void TC0_Handler(void) { - tud_task(); - cdc_task(); - TC0->COUNT8.INTFLAG.reg |= TC_INTFLAG_OVF; -} - - // USB Descriptors and tinyUSB callbacks follow. /* diff --git a/watch-library/hardware/watch/watch_private_cdc.c b/watch-library/hardware/watch/watch_private_cdc.c new file mode 100644 index 000000000..a961b5ed7 --- /dev/null +++ b/watch-library/hardware/watch/watch_private_cdc.c @@ -0,0 +1,160 @@ +/* + * MIT License + * + * Copyright (c) 2020 Joey Castillo + * Copyright (c) 2023 Edward Shin + * + * 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 "watch_private_cdc.h" + +#include + +#include "watch_utility.h" +#include "tusb.h" + +/* + * Implement a circular buffer for the USB CDC Serial read buffer. + * The size of the buffer must be a power of two for this circular buffer + * implementation to work. + */ + +// Size of the circular buffer. Must be a power of two. +#define CDC_WRITE_BUF_SZ (1024) +// Macro function to perform modular arithmetic on an index. +// eg. (63 + 2) & (64 - 1) -> 1 +#define CDC_WRITE_BUF_IDX(x) ((x) & (CDC_WRITE_BUF_SZ - 1)) +static char s_write_buf[CDC_WRITE_BUF_SZ] = {0}; +static size_t s_write_buf_pos = 0; +static size_t s_write_buf_len = 0; + +#define CDC_READ_BUF_SZ (256) +#define CDC_READ_BUF_IDX(x) ((x) & (CDC_READ_BUF_SZ - 1)) +static char s_read_buf[CDC_READ_BUF_SZ] = {0}; +static size_t s_read_buf_pos = 0; +static size_t s_read_buf_len = 0; + +// Mask TC1 interrupts, preventing calls to cdc_task() +static inline void prv_critical_section_enter(void) { + NVIC_DisableIRQ(TC1_IRQn); +} + +// Unmask TC1 interrupts, allowing calls to cdc_task() +static inline void prv_critical_section_exit(void) { + NVIC_EnableIRQ(TC1_IRQn); +} + +int _write(int file, char *ptr, int len) { + (void) file; + + if (ptr == NULL || len <= 0) { + return -1; + } + + int bytes_written = 0; + + prv_critical_section_enter(); + + for (int i = 0; i < len; i++) { + s_write_buf[s_write_buf_pos] = ptr[i]; + s_write_buf_pos = CDC_WRITE_BUF_IDX(s_write_buf_pos + 1); + if (s_write_buf_len < CDC_WRITE_BUF_SZ) { + s_write_buf_len++; + } + bytes_written++; + } + + prv_critical_section_exit(); + + return bytes_written; +} + +int _read(int file, char *ptr, int len) { + (void) file; + + prv_critical_section_enter(); + + if (ptr == NULL || len <= 0 || s_read_buf_len == 0) { + prv_critical_section_exit(); + return -1; + } + + // Clamp to the length of the read buffer + if ((size_t) len > s_read_buf_len) { + len = s_read_buf_len; + } + + // Calculate the start of the circular buffer, and iterate from there + const size_t start_pos = CDC_READ_BUF_IDX(s_read_buf_pos - len); + for (size_t i = 0; i < (size_t) len; i++) { + const size_t idx = CDC_READ_BUF_IDX(start_pos + i); + ptr[i] = s_read_buf[idx]; + s_read_buf[idx] = 0; + } + + // Update circular buffer position and length + s_read_buf_len -= len; + s_read_buf_pos = CDC_READ_BUF_IDX(s_read_buf_pos - len); + + prv_critical_section_exit(); + + return len; +} + +static void prv_handle_reads(void) { + while (tud_cdc_available()) { + int c = tud_cdc_read_char(); + if (c < 0) { + continue; + } + s_read_buf[s_read_buf_pos] = c; + s_read_buf_pos = CDC_READ_BUF_IDX(s_read_buf_pos + 1); + if (s_read_buf_len < CDC_READ_BUF_SZ) { + s_read_buf_len++; + } + } +} + +static void prv_handle_writes(void) { + if (s_write_buf_len > 0) { + const size_t start_pos = + CDC_WRITE_BUF_IDX(s_write_buf_pos - s_write_buf_len); + for (size_t i = 0; i < (size_t) s_write_buf_len; i++) { + const size_t idx = CDC_WRITE_BUF_IDX(start_pos + i); + if (tud_cdc_available() > 0) { + // If we receive data while doing a large write, we need to + // fully service it before continuing to write, or the + // stack will crash. + prv_handle_reads(); + } + if (tud_cdc_write_available()) { + tud_cdc_write(&s_write_buf[idx], 1); + } + s_write_buf[idx] = 0; + s_write_buf_len--; + } + tud_cdc_write_flush(); + } +} + +void cdc_task(void) { + prv_handle_reads(); + prv_handle_writes(); +} diff --git a/watch-library/hardware/watch/watch_private_cdc.h b/watch-library/hardware/watch/watch_private_cdc.h new file mode 100644 index 000000000..b7fa95854 --- /dev/null +++ b/watch-library/hardware/watch/watch_private_cdc.h @@ -0,0 +1,33 @@ +/* + * MIT License + * + * Copyright (c) 2020 Joey Castillo + * Copyright (c) 2023 Edward Shin + * + * 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 _WATCH_PRIVATE_CDC_H_INCLUDED +#define _WATCH_PRIVATE_CDC_H_INCLUDED + +int _write(int file, char *ptr, int len); +int _read(int file, char *ptr, int len); +void cdc_task(void); + +#endif diff --git a/watch-library/shared/watch/watch.h b/watch-library/shared/watch/watch.h index 790f9a163..2f697f0aa 100644 --- a/watch-library/shared/watch/watch.h +++ b/watch-library/shared/watch/watch.h @@ -88,6 +88,10 @@ bool watch_is_usb_enabled(void); */ void watch_reset_to_bootloader(void); +/** @brief Call periodically from app main loop to service CDC RX/TX. + */ +void cdc_task(void); + /** @brief Reads up to len bytes from the USB serial. * @param file ignored, you can pass in 0 * @param ptr pointer to a buffer of at least len bytes @@ -96,4 +100,4 @@ void watch_reset_to_bootloader(void); */ int read(int file, char *ptr, int len); -#endif /* WATCH_H_ */ \ No newline at end of file +#endif /* WATCH_H_ */ diff --git a/watch-library/shared/watch/watch_private.h b/watch-library/shared/watch/watch_private.h index 9d55bc212..8fcc57554 100644 --- a/watch-library/shared/watch/watch_private.h +++ b/watch-library/shared/watch/watch_private.h @@ -38,14 +38,19 @@ void _watch_enable_tcc(void); /// Called by buzzer and LED teardown functions. You should not call this from your app. void _watch_disable_tcc(void); -/// Called by main.c if plugged in to USB. You should not call this from your app. -void _watch_enable_usb(void); +/// Enable USB task timer. Called by USB enable routine in main(). You should not call this from your app. +void _watch_enable_tc0(void); + +/// Disable USB task timer. You should not call this from your app. +void _watch_disable_tc0(void); -// this function ends up getting called by printf to log stuff to the USB console. -int _write(int file, char *ptr, int len); +/// Enable CDC task timer. Called by USB enable routine in main(). You should not call this from your app. +void _watch_enable_tc1(void); -// i thought this would be called by gets but it doesn't? anyway it does get called by read() -// so that's our mechanism for reading data from the USB serial console. -int _read(int file, char *ptr, int len); +/// Disable CDC task timer. You should not call this from your app. +void _watch_disable_tc1(void); + +/// Called by main.c if plugged in to USB. You should not call this from your app. +void _watch_enable_usb(void); #endif